Skip to content

Commit b4b402b

Browse files
mdesmetclaude
andcommitted
feat(dbt-tools): auto-discover config, expose ALTIMATE_CODE_* env vars for vscode integration
- config.ts: add findProjectRoot() and discoverPython() (exported), update read() to auto-discover projectRoot and pythonPath at runtime when no config file exists — removes requirement to run `altimate-dbt init` before using any command - discoverPython() prioritises ALTIMATE_CODE_VIRTUAL_ENV (injected by vscode-altimate-mcp-server) over project-local venvs; tries python3 before python in each candidate location - dbt-resolve.ts: add tier 2 (ALTIMATE_CODE_PYTHON_PATH sibling) and tier 6 (ALTIMATE_CODE_VIRTUAL_ENV) so the dbt binary is found when altimate serve is spawned with a vscode-activated Python environment - init.ts: remove duplicated find()/python() helpers; import from config.ts - tests: add config.test.ts coverage for auto-discovery, findProjectRoot, discoverPython; add dbt-resolve.test.ts scenarios for ALTIMATE_CODE_PYTHON_PATH and ALTIMATE_CODE_VIRTUAL_ENV priority Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ad99c5c commit b4b402b

5 files changed

Lines changed: 360 additions & 75 deletions

File tree

packages/dbt-tools/src/commands/init.ts

Lines changed: 4 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,21 @@
1-
import { join, resolve } from "path"
1+
import { resolve, join } from "path"
22
import { existsSync } from "fs"
3-
import { execFileSync } from "child_process"
4-
import { write, type Config } from "../config"
3+
import { write, findProjectRoot, discoverPython, type Config } from "../config"
54
import { all } from "../check"
65

7-
function find(start: string): string | null {
8-
let dir = resolve(start)
9-
while (true) {
10-
if (existsSync(join(dir, "dbt_project.yml"))) return dir
11-
const parent = resolve(dir, "..")
12-
if (parent === dir) return null
13-
dir = parent
14-
}
15-
}
16-
17-
/**
18-
* Discover the Python binary, checking multiple environment managers.
19-
*
20-
* Priority:
21-
* 1. Project-local .venv/bin/python (uv, pdm, venv, poetry in-project)
22-
* 2. VIRTUAL_ENV/bin/python (activated venv)
23-
* 3. CONDA_PREFIX/bin/python (conda)
24-
* 4. `which python3` / `which python` (system PATH)
25-
* 5. Fallback "python3" (hope for the best)
26-
*/
27-
function python(projectRoot?: string): string {
28-
// Check project-local venvs first (most reliable for dbt projects)
29-
if (projectRoot) {
30-
for (const venvDir of [".venv", "venv", "env"]) {
31-
const py = join(projectRoot, venvDir, "bin", "python")
32-
if (existsSync(py)) return py
33-
}
34-
}
35-
36-
// Check VIRTUAL_ENV (set by activate scripts)
37-
const virtualEnv = process.env.VIRTUAL_ENV
38-
if (virtualEnv) {
39-
const py = join(virtualEnv, "bin", "python")
40-
if (existsSync(py)) return py
41-
}
42-
43-
// Check CONDA_PREFIX (set by conda activate)
44-
const condaPrefix = process.env.CONDA_PREFIX
45-
if (condaPrefix) {
46-
const py = join(condaPrefix, "bin", "python")
47-
if (existsSync(py)) return py
48-
}
49-
50-
// Fall back to PATH-based discovery
51-
for (const cmd of ["python3", "python"]) {
52-
try {
53-
return execFileSync("which", [cmd], { encoding: "utf-8" }).trim()
54-
} catch {}
55-
}
56-
return "python3"
57-
}
58-
596
export async function init(args: string[]) {
607
const idx = args.indexOf("--project-root")
618
const root = idx >= 0 ? args[idx + 1] : undefined
629
const pidx = args.indexOf("--python-path")
6310
const py = pidx >= 0 ? args[pidx + 1] : undefined
6411

65-
const project = root ? resolve(root) : find(process.cwd())
12+
const project = root ? resolve(root) : findProjectRoot(process.cwd())
6613
if (!project) return { error: "No dbt_project.yml found. Use --project-root to specify." }
6714
if (!existsSync(join(project, "dbt_project.yml"))) return { error: `No dbt_project.yml in ${project}` }
6815

6916
const cfg: Config = {
7017
projectRoot: project,
71-
pythonPath: py ?? python(project),
18+
pythonPath: py ?? discoverPython(project),
7219
dbtIntegration: "corecommand",
7320
queryLimit: 500,
7421
}

packages/dbt-tools/src/config.ts

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { homedir } from "os"
2-
import { join } from "path"
2+
import { join, resolve } from "path"
33
import { readFile, writeFile, mkdir } from "fs/promises"
44
import { existsSync } from "fs"
5+
import { execFileSync } from "child_process"
56

67
type Config = {
78
projectRoot: string
@@ -18,11 +19,84 @@ function configPath() {
1819
return join(configDir(), "dbt.json")
1920
}
2021

22+
/**
23+
* Walk up from `start` to find the nearest directory containing dbt_project.yml.
24+
* Returns null if none found.
25+
*/
26+
export function findProjectRoot(start = process.cwd()): string | null {
27+
let dir = resolve(start)
28+
while (true) {
29+
if (existsSync(join(dir, "dbt_project.yml"))) return dir
30+
const parent = resolve(dir, "..")
31+
if (parent === dir) return null
32+
dir = parent
33+
}
34+
}
35+
36+
/**
37+
* Discover the Python binary for a given project root.
38+
* Priority: ALTIMATE_CODE_VIRTUAL_ENV → project-local .venv → VIRTUAL_ENV → CONDA_PREFIX → which python3
39+
*/
40+
export function discoverPython(projectRoot: string): string {
41+
// ALTIMATE_CODE_VIRTUAL_ENV (injected by vscode-altimate-mcp-server — explicit user selection wins)
42+
const altVenv = process.env.ALTIMATE_CODE_VIRTUAL_ENV
43+
if (altVenv) {
44+
for (const bin of ["python3", "python"]) {
45+
const py = join(altVenv, "bin", bin)
46+
if (existsSync(py)) return py
47+
}
48+
}
49+
50+
// Project-local venvs (uv, pdm, venv, poetry in-project, rye)
51+
for (const venvDir of [".venv", "venv", "env"]) {
52+
for (const bin of ["python3", "python"]) {
53+
const py = join(projectRoot, venvDir, "bin", bin)
54+
if (existsSync(py)) return py
55+
}
56+
}
57+
58+
// VIRTUAL_ENV (set by activate scripts)
59+
const virtualEnv = process.env.VIRTUAL_ENV
60+
if (virtualEnv) {
61+
for (const bin of ["python3", "python"]) {
62+
const py = join(virtualEnv, "bin", bin)
63+
if (existsSync(py)) return py
64+
}
65+
}
66+
67+
// CONDA_PREFIX
68+
const condaPrefix = process.env.CONDA_PREFIX
69+
if (condaPrefix) {
70+
for (const bin of ["python3", "python"]) {
71+
const py = join(condaPrefix, "bin", bin)
72+
if (existsSync(py)) return py
73+
}
74+
}
75+
76+
// PATH-based discovery
77+
for (const cmd of ["python3", "python"]) {
78+
try {
79+
return execFileSync("which", [cmd], { encoding: "utf-8" }).trim()
80+
} catch {}
81+
}
82+
return "python3"
83+
}
84+
2185
async function read(): Promise<Config | null> {
2286
const p = configPath()
23-
if (!existsSync(p)) return null
24-
const raw = await readFile(p, "utf-8")
25-
return JSON.parse(raw) as Config
87+
if (existsSync(p)) {
88+
const raw = await readFile(p, "utf-8")
89+
return JSON.parse(raw) as Config
90+
}
91+
// No config file — auto-discover from cwd so `altimate-dbt init` isn't required
92+
const projectRoot = findProjectRoot()
93+
if (!projectRoot) return null
94+
return {
95+
projectRoot,
96+
pythonPath: discoverPython(projectRoot),
97+
dbtIntegration: "corecommand",
98+
queryLimit: 500,
99+
}
26100
}
27101

28102
async function write(cfg: Config) {

packages/dbt-tools/src/dbt-resolve.ts

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,15 @@ export interface ResolvedDbt {
4343
*
4444
* Priority:
4545
* 1. ALTIMATE_DBT_PATH env var (explicit user override)
46-
* 2. Sibling of configured pythonPath (same venv/bin)
47-
* 3. Project-local .venv/bin/dbt (uv, pdm, venv, rye, poetry in-project)
48-
* 4. CONDA_PREFIX/bin/dbt (conda environments)
49-
* 5. VIRTUAL_ENV/bin/dbt (activated venv)
50-
* 6. Pyenv real path resolution (follow shims)
51-
* 7. `which dbt` on current PATH
52-
* 8. Common known locations (~/.local/bin/dbt for pipx, etc.)
46+
* 2. Sibling of ALTIMATE_CODE_PYTHON_PATH (set by vscode-altimate-mcp-server)
47+
* 3. Sibling of configured pythonPath (same venv/bin)
48+
* 4. Project-local .venv/bin/dbt (uv, pdm, venv, rye, poetry in-project)
49+
* 5. CONDA_PREFIX/bin/dbt (conda environments)
50+
* 6. ALTIMATE_CODE_VIRTUAL_ENV/bin/dbt (set by vscode-altimate-mcp-server)
51+
* 7. VIRTUAL_ENV/bin/dbt (activated venv)
52+
* 8. Pyenv real path resolution (follow shims)
53+
* 9. `which dbt` on current PATH
54+
* 10. Common known locations (~/.local/bin/dbt for pipx, etc.)
5355
*
5456
* Each candidate is validated by checking it exists and is executable.
5557
*/
@@ -62,7 +64,14 @@ export function resolveDbt(pythonPath?: string, projectRoot?: string): ResolvedD
6264
candidates.push({ path: envOverride, source: "ALTIMATE_DBT_PATH env var" })
6365
}
6466

65-
// 2. Sibling of configured pythonPath (most common: venv, conda, pyenv real path)
67+
// 2. Sibling of ALTIMATE_CODE_PYTHON_PATH (injected by vscode-altimate-mcp-server)
68+
const altPython = process.env.ALTIMATE_CODE_PYTHON_PATH
69+
if (altPython) {
70+
const binDir = dirname(altPython)
71+
candidates.push({ path: join(binDir, "dbt"), source: "sibling of ALTIMATE_CODE_PYTHON_PATH", binDir })
72+
}
73+
74+
// 3. Sibling of configured pythonPath (most common: venv, conda, pyenv real path)
6675
if (pythonPath && existsSync(pythonPath)) {
6776
const binDir = dirname(pythonPath)
6877
const siblingDbt = join(binDir, "dbt")
@@ -87,7 +96,7 @@ export function resolveDbt(pythonPath?: string, projectRoot?: string): ResolvedD
8796
}
8897
}
8998

90-
// 4. CONDA_PREFIX (conda/mamba/micromamba — set after `conda activate`)
99+
// 5. CONDA_PREFIX (conda/mamba/micromamba — set after `conda activate`)
91100
const condaPrefix = process.env.CONDA_PREFIX
92101
if (condaPrefix) {
93102
candidates.push({
@@ -97,7 +106,17 @@ export function resolveDbt(pythonPath?: string, projectRoot?: string): ResolvedD
97106
})
98107
}
99108

100-
// 5. VIRTUAL_ENV (set by venv/virtualenv activate scripts)
109+
// 6. ALTIMATE_CODE_VIRTUAL_ENV (injected by vscode-altimate-mcp-server, avoids conflicts with user's VIRTUAL_ENV)
110+
const altVenv = process.env.ALTIMATE_CODE_VIRTUAL_ENV
111+
if (altVenv) {
112+
candidates.push({
113+
path: join(altVenv, "bin", "dbt"),
114+
source: `ALTIMATE_CODE_VIRTUAL_ENV (${altVenv})`,
115+
binDir: join(altVenv, "bin"),
116+
})
117+
}
118+
119+
// 7. VIRTUAL_ENV (set by venv/virtualenv activate scripts)
101120
const virtualEnv = process.env.VIRTUAL_ENV
102121
if (virtualEnv) {
103122
candidates.push({
@@ -110,7 +129,7 @@ export function resolveDbt(pythonPath?: string, projectRoot?: string): ResolvedD
110129
// Helper: current process env (for subprocess calls that need to inherit it)
111130
const currentEnv = { ...process.env }
112131

113-
// 6. Pyenv: resolve through shim to real binary
132+
// 8. Pyenv: resolve through shim to real binary
114133
const pyenvRoot = process.env.PYENV_ROOT ?? join(process.env.HOME ?? "", ".pyenv")
115134
if (existsSync(join(pyenvRoot, "shims", "dbt"))) {
116135
try {
@@ -128,7 +147,7 @@ export function resolveDbt(pythonPath?: string, projectRoot?: string): ResolvedD
128147
}
129148
}
130149

131-
// 7. asdf/mise shim resolution
150+
// 9. asdf/mise shim resolution
132151
const asdfDataDir = process.env.ASDF_DATA_DIR ?? join(process.env.HOME ?? "", ".asdf")
133152
if (existsSync(join(asdfDataDir, "shims", "dbt"))) {
134153
try {

0 commit comments

Comments
 (0)