Skip to content

Commit 0e6bb70

Browse files
mdesmetclaudeanandgupta42
authored
feat(dbt-tools): auto-discover config with Windows compatibility (#464)
* 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> * fix(dbt-tools): address code review findings for config auto-discovery - Add `ALTIMATE_CODE_PYTHON_PATH` as highest priority in `discoverPython()` so VS Code's explicit Python selection is not silently ignored - Align `ALTIMATE_CODE_VIRTUAL_ENV` priority: move to tier 4 in `resolveDbt()` (before project-local `.venv`) to match `discoverPython()` priority and prevent Python/dbt resolving from different environments - Add `JSON.parse` error handling in `read()` — malformed config file now falls back to auto-discovery instead of crashing - Fix stale inline comment numbering in `dbt-resolve.ts` (1-11) - Fix environment-dependent `read returns null` test by setting CWD to temp dir - Add tests: `ALTIMATE_CODE_PYTHON_PATH` priority, malformed config fallback - Run prettier on `dbt-resolve.ts` and `dbt-resolve.test.ts` Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(dbt-tools): remove ALTIMATE_CODE_* env vars and add Windows compatibility Drop ALTIMATE_CODE_PYTHON_PATH and ALTIMATE_CODE_VIRTUAL_ENV — dbt and Python will be on PATH anyway. Add Windows support: use Scripts/ instead of bin/, .exe suffix for binaries, where instead of which, skip Unix-only pyenv/asdf resolution, and add Windows-specific known fallback paths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(dbt-tools): handle undefined from array index in discoverPython Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(dbt-tools): fix PATH separator and Conda discovery for Windows - Use `path.delimiter` instead of hardcoded `:` in `validateDbt()` and `buildDbtEnv()` so PATH injection works on Windows (`;` separator) - Fix Conda Python discovery on Windows: check env root directly since Conda places `python.exe` at `%CONDA_PREFIX%\python.exe`, not in `Scripts/` - Add `timeout: 5_000` to `execFileSync` in `discoverPython()` to match the same pattern used in `dbt-resolve.ts` Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: anandgupta42 <anand@altimate.ai>
1 parent 79c34c5 commit 0e6bb70

5 files changed

Lines changed: 330 additions & 129 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: 86 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,92 @@ 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+
const isWindows = process.platform === "win32"
37+
// Windows venvs use Scripts/, Unix venvs use bin/
38+
const VENV_BIN = isWindows ? "Scripts" : "bin"
39+
// Windows executables have .exe suffix
40+
const EXE = isWindows ? ".exe" : ""
41+
42+
/**
43+
* Discover the Python binary for a given project root.
44+
* Priority: project-local .venv → VIRTUAL_ENV → CONDA_PREFIX → which/where python
45+
*/
46+
export function discoverPython(projectRoot: string): string {
47+
// Candidate Python binary names (python3 first on Unix; python.exe on Windows)
48+
const pythonBins = isWindows ? ["python.exe", "python3.exe"] : ["python3", "python"]
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 pythonBins) {
53+
const py = join(projectRoot, venvDir, VENV_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 pythonBins) {
62+
const py = join(virtualEnv, VENV_BIN, bin)
63+
if (existsSync(py)) return py
64+
}
65+
}
66+
67+
// CONDA_PREFIX (Conda places python at env root on Windows, bin/ on Unix)
68+
const condaPrefix = process.env.CONDA_PREFIX
69+
if (condaPrefix) {
70+
for (const bin of pythonBins) {
71+
const py = isWindows ? join(condaPrefix, bin) : join(condaPrefix, VENV_BIN, bin)
72+
if (existsSync(py)) return py
73+
}
74+
}
75+
76+
// PATH-based discovery (`where` on Windows, `which` on Unix)
77+
const whichCmd = isWindows ? "where" : "which"
78+
const cmds = isWindows ? ["python.exe", "python3.exe", "python"] : ["python3", "python"]
79+
for (const cmd of cmds) {
80+
try {
81+
// `where` on Windows may return multiple lines — take the first
82+
const first = execFileSync(whichCmd, [cmd], { encoding: "utf-8", timeout: 5_000 }).trim().split(/\r?\n/)[0]
83+
if (first) return first
84+
} catch {}
85+
}
86+
return isWindows ? "python.exe" : "python3"
87+
}
88+
2189
async function read(): Promise<Config | null> {
2290
const p = configPath()
23-
if (!existsSync(p)) return null
24-
const raw = await readFile(p, "utf-8")
25-
return JSON.parse(raw) as Config
91+
if (existsSync(p)) {
92+
try {
93+
const raw = await readFile(p, "utf-8")
94+
return JSON.parse(raw) as Config
95+
} catch {
96+
// Malformed config — fall through to auto-discovery
97+
}
98+
}
99+
// No config file — auto-discover from cwd so `altimate-dbt init` isn't required
100+
const projectRoot = findProjectRoot()
101+
if (!projectRoot) return null
102+
return {
103+
projectRoot,
104+
pythonPath: discoverPython(projectRoot),
105+
dbtIntegration: "corecommand",
106+
queryLimit: 500,
107+
}
26108
}
27109

28110
async function write(cfg: Config) {

0 commit comments

Comments
 (0)