Skip to content

Commit c209d46

Browse files
mdesmetclaude
andcommitted
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>
1 parent fdb8519 commit c209d46

4 files changed

Lines changed: 112 additions & 277 deletions

File tree

packages/dbt-tools/src/config.ts

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -33,57 +33,56 @@ export function findProjectRoot(start = process.cwd()): string | null {
3333
}
3434
}
3535

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+
3642
/**
3743
* Discover the Python binary for a given project root.
38-
* Priority: ALTIMATE_CODE_PYTHON_PATH → ALTIMATE_CODE_VIRTUAL_ENV → project-local .venv → VIRTUAL_ENV → CONDA_PREFIX → which python3
44+
* Priority: project-local .venv → VIRTUAL_ENV → CONDA_PREFIX → which/where python
3945
*/
4046
export function discoverPython(projectRoot: string): string {
41-
// ALTIMATE_CODE_PYTHON_PATH (explicit selection from vscode-altimate-mcp-server — highest priority)
42-
const altPython = process.env.ALTIMATE_CODE_PYTHON_PATH
43-
if (altPython && existsSync(altPython)) return altPython
44-
45-
// ALTIMATE_CODE_VIRTUAL_ENV (injected by vscode-altimate-mcp-server — explicit user selection wins)
46-
const altVenv = process.env.ALTIMATE_CODE_VIRTUAL_ENV
47-
if (altVenv) {
48-
for (const bin of ["python3", "python"]) {
49-
const py = join(altVenv, "bin", bin)
50-
if (existsSync(py)) return py
51-
}
52-
}
47+
// Candidate Python binary names (python3 first on Unix; python.exe on Windows)
48+
const pythonBins = isWindows ? ["python.exe", "python3.exe"] : ["python3", "python"]
5349

5450
// Project-local venvs (uv, pdm, venv, poetry in-project, rye)
5551
for (const venvDir of [".venv", "venv", "env"]) {
56-
for (const bin of ["python3", "python"]) {
57-
const py = join(projectRoot, venvDir, "bin", bin)
52+
for (const bin of pythonBins) {
53+
const py = join(projectRoot, venvDir, VENV_BIN, bin)
5854
if (existsSync(py)) return py
5955
}
6056
}
6157

6258
// VIRTUAL_ENV (set by activate scripts)
6359
const virtualEnv = process.env.VIRTUAL_ENV
6460
if (virtualEnv) {
65-
for (const bin of ["python3", "python"]) {
66-
const py = join(virtualEnv, "bin", bin)
61+
for (const bin of pythonBins) {
62+
const py = join(virtualEnv, VENV_BIN, bin)
6763
if (existsSync(py)) return py
6864
}
6965
}
7066

7167
// CONDA_PREFIX
7268
const condaPrefix = process.env.CONDA_PREFIX
7369
if (condaPrefix) {
74-
for (const bin of ["python3", "python"]) {
75-
const py = join(condaPrefix, "bin", bin)
70+
for (const bin of pythonBins) {
71+
const py = join(condaPrefix, VENV_BIN, bin)
7672
if (existsSync(py)) return py
7773
}
7874
}
7975

80-
// PATH-based discovery
81-
for (const cmd of ["python3", "python"]) {
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) {
8280
try {
83-
return execFileSync("which", [cmd], { encoding: "utf-8" }).trim()
81+
// `where` on Windows may return multiple lines — take the first
82+
return execFileSync(whichCmd, [cmd], { encoding: "utf-8" }).trim().split(/\r?\n/)[0]
8483
} catch {}
8584
}
86-
return "python3"
85+
return isWindows ? "python.exe" : "python3"
8786
}
8887

8988
async function read(): Promise<Config | null> {

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

Lines changed: 85 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ import { execFileSync } from "child_process"
2929
import { existsSync, realpathSync, readFileSync } from "fs"
3030
import { dirname, join } from "path"
3131

32+
const isWindows = process.platform === "win32"
33+
// Windows venvs use Scripts/, Unix venvs use bin/
34+
const VENV_BIN = isWindows ? "Scripts" : "bin"
35+
// Windows executables have .exe suffix
36+
const EXE = isWindows ? ".exe" : ""
37+
3238
export interface ResolvedDbt {
3339
/** Absolute path to the dbt binary (or "dbt" if relying on PATH). */
3440
path: string
@@ -43,16 +49,14 @@ export interface ResolvedDbt {
4349
*
4450
* Priority:
4551
* 1. ALTIMATE_DBT_PATH env var (explicit user override)
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. ALTIMATE_CODE_VIRTUAL_ENV/bin/dbt (set by vscode-altimate-mcp-server)
49-
* 5. Project-local .venv/bin/dbt (uv, pdm, venv, rye, poetry in-project)
50-
* 6. CONDA_PREFIX/bin/dbt (conda environments)
51-
* 7. VIRTUAL_ENV/bin/dbt (activated venv)
52-
* 8. Pyenv real path resolution (follow shims)
53-
* 9. asdf/mise shim resolution
54-
* 10. `which dbt` on current PATH
55-
* 11. Common known locations (~/.local/bin/dbt for pipx, etc.)
52+
* 2. Sibling of configured pythonPath (same venv/Scripts or venv/bin)
53+
* 3. Project-local .venv/bin/dbt or .venv/Scripts/dbt.exe
54+
* 4. CONDA_PREFIX/bin/dbt or Scripts/dbt.exe (conda environments)
55+
* 5. VIRTUAL_ENV/bin/dbt or Scripts/dbt.exe (activated venv)
56+
* 6. Pyenv real path resolution — Unix only (follow shims)
57+
* 7. asdf/mise shim resolution — Unix only
58+
* 8. `which`/`where dbt` on current PATH
59+
* 9. Common known locations (~/.local/bin/dbt for pipx, etc.)
5660
*
5761
* Each candidate is validated by checking it exists and is executable.
5862
*/
@@ -65,127 +69,127 @@ export function resolveDbt(pythonPath?: string, projectRoot?: string): ResolvedD
6569
candidates.push({ path: envOverride, source: "ALTIMATE_DBT_PATH env var" })
6670
}
6771

68-
// 2. Sibling of ALTIMATE_CODE_PYTHON_PATH (injected by vscode-altimate-mcp-server)
69-
const altPython = process.env.ALTIMATE_CODE_PYTHON_PATH
70-
if (altPython) {
71-
const binDir = dirname(altPython)
72-
candidates.push({ path: join(binDir, "dbt"), source: "sibling of ALTIMATE_CODE_PYTHON_PATH", binDir })
73-
}
74-
75-
// 3. Sibling of configured pythonPath (most common: venv, conda, pyenv real path)
72+
// 2. Sibling of configured pythonPath (most common: venv, conda, pyenv real path)
7673
if (pythonPath && existsSync(pythonPath)) {
7774
const binDir = dirname(pythonPath)
78-
const siblingDbt = join(binDir, "dbt")
75+
const siblingDbt = join(binDir, `dbt${EXE}`)
7976
candidates.push({ path: siblingDbt, source: `sibling of pythonPath (${pythonPath})`, binDir })
8077

8178
// If pythonPath is a symlink (e.g., pyenv shim), also check the real path
8279
try {
8380
const realPython = realpathSync(pythonPath)
8481
if (realPython !== pythonPath) {
8582
const realBinDir = dirname(realPython)
86-
const realDbt = join(realBinDir, "dbt")
83+
const realDbt = join(realBinDir, `dbt${EXE}`)
8784
candidates.push({ path: realDbt, source: `real path of pythonPath (${realPython})`, binDir: realBinDir })
8885
}
8986
} catch {}
9087
}
9188

92-
// 4. ALTIMATE_CODE_VIRTUAL_ENV (injected by vscode-altimate-mcp-server, avoids conflicts with user's VIRTUAL_ENV)
93-
const altVenv = process.env.ALTIMATE_CODE_VIRTUAL_ENV
94-
if (altVenv) {
95-
candidates.push({
96-
path: join(altVenv, "bin", "dbt"),
97-
source: `ALTIMATE_CODE_VIRTUAL_ENV (${altVenv})`,
98-
binDir: join(altVenv, "bin"),
99-
})
100-
}
101-
102-
// 5. Project-local .venv/bin/dbt (uv, pdm, venv, poetry in-project, rye)
89+
// 3. Project-local .venv/Scripts/dbt.exe (Windows) or .venv/bin/dbt (Unix)
10390
if (projectRoot) {
10491
for (const venvDir of [".venv", "venv", "env"]) {
105-
const localDbt = join(projectRoot, venvDir, "bin", "dbt")
92+
const localDbt = join(projectRoot, venvDir, VENV_BIN, `dbt${EXE}`)
10693
candidates.push({
10794
path: localDbt,
10895
source: `${venvDir}/ in project root`,
109-
binDir: join(projectRoot, venvDir, "bin"),
96+
binDir: join(projectRoot, venvDir, VENV_BIN),
11097
})
11198
}
11299
}
113100

114-
// 6. CONDA_PREFIX (conda/mamba/micromamba — set after `conda activate`)
101+
// 4. CONDA_PREFIX (conda/mamba/micromamba — set after `conda activate`)
115102
const condaPrefix = process.env.CONDA_PREFIX
116103
if (condaPrefix) {
117104
candidates.push({
118-
path: join(condaPrefix, "bin", "dbt"),
105+
path: join(condaPrefix, VENV_BIN, `dbt${EXE}`),
119106
source: `CONDA_PREFIX (${condaPrefix})`,
120-
binDir: join(condaPrefix, "bin"),
107+
binDir: join(condaPrefix, VENV_BIN),
121108
})
122109
}
123110

124-
// 7. VIRTUAL_ENV (set by venv/virtualenv activate scripts)
111+
// 5. VIRTUAL_ENV (set by venv/virtualenv activate scripts)
125112
const virtualEnv = process.env.VIRTUAL_ENV
126113
if (virtualEnv) {
127114
candidates.push({
128-
path: join(virtualEnv, "bin", "dbt"),
115+
path: join(virtualEnv, VENV_BIN, `dbt${EXE}`),
129116
source: `VIRTUAL_ENV (${virtualEnv})`,
130-
binDir: join(virtualEnv, "bin"),
117+
binDir: join(virtualEnv, VENV_BIN),
131118
})
132119
}
133120

134121
// Helper: current process env (for subprocess calls that need to inherit it)
135122
const currentEnv = { ...process.env }
136123

137-
// 8. Pyenv: resolve through shim to real binary
138-
const pyenvRoot = process.env.PYENV_ROOT ?? join(process.env.HOME ?? "", ".pyenv")
139-
if (existsSync(join(pyenvRoot, "shims", "dbt"))) {
140-
try {
141-
// `pyenv which dbt` resolves the shim to the actual binary path
142-
const realDbt = execFileSync("pyenv", ["which", "dbt"], {
143-
encoding: "utf-8",
144-
timeout: 5_000,
145-
env: { ...currentEnv, PYENV_ROOT: pyenvRoot },
146-
}).trim()
147-
if (realDbt) {
148-
candidates.push({ path: realDbt, source: `pyenv which dbt`, binDir: dirname(realDbt) })
124+
if (!isWindows) {
125+
// 6. Pyenv: resolve through shim to real binary (Unix only)
126+
const pyenvRoot = process.env.PYENV_ROOT ?? join(process.env.HOME ?? "", ".pyenv")
127+
if (existsSync(join(pyenvRoot, "shims", "dbt"))) {
128+
try {
129+
// `pyenv which dbt` resolves the shim to the actual binary path
130+
const realDbt = execFileSync("pyenv", ["which", "dbt"], {
131+
encoding: "utf-8",
132+
timeout: 5_000,
133+
env: { ...currentEnv, PYENV_ROOT: pyenvRoot },
134+
}).trim()
135+
if (realDbt) {
136+
candidates.push({ path: realDbt, source: `pyenv which dbt`, binDir: dirname(realDbt) })
137+
}
138+
} catch {
139+
// pyenv not functional — shim won't resolve
149140
}
150-
} catch {
151-
// pyenv not functional — shim won't resolve
152141
}
153-
}
154142

155-
// 9. asdf/mise shim resolution
156-
const asdfDataDir = process.env.ASDF_DATA_DIR ?? join(process.env.HOME ?? "", ".asdf")
157-
if (existsSync(join(asdfDataDir, "shims", "dbt"))) {
158-
try {
159-
const realDbt = execFileSync("asdf", ["which", "dbt"], {
160-
encoding: "utf-8",
161-
timeout: 5_000,
162-
env: currentEnv,
163-
}).trim()
164-
if (realDbt) {
165-
candidates.push({ path: realDbt, source: `asdf which dbt`, binDir: dirname(realDbt) })
166-
}
167-
} catch {}
143+
// 7. asdf/mise shim resolution (Unix only)
144+
const asdfDataDir = process.env.ASDF_DATA_DIR ?? join(process.env.HOME ?? "", ".asdf")
145+
if (existsSync(join(asdfDataDir, "shims", "dbt"))) {
146+
try {
147+
const realDbt = execFileSync("asdf", ["which", "dbt"], {
148+
encoding: "utf-8",
149+
timeout: 5_000,
150+
env: currentEnv,
151+
}).trim()
152+
if (realDbt) {
153+
candidates.push({ path: realDbt, source: `asdf which dbt`, binDir: dirname(realDbt) })
154+
}
155+
} catch {}
156+
}
168157
}
169158

170-
// 10. `which dbt` on current PATH (catches pipx ~/.local/bin, system pip, homebrew, etc.)
159+
// 8. `where dbt` (Windows) / `which dbt` (Unix) on current PATH
160+
const whichCmd = isWindows ? "where" : "which"
161+
const dbtCmd = `dbt${EXE}`
171162
try {
172-
const whichDbt = execFileSync("which", ["dbt"], {
163+
const found = execFileSync(whichCmd, [dbtCmd], {
173164
encoding: "utf-8",
174165
timeout: 5_000,
175166
env: currentEnv,
176-
}).trim()
177-
if (whichDbt) {
178-
candidates.push({ path: whichDbt, source: `which dbt (PATH)`, binDir: dirname(whichDbt) })
167+
})
168+
.trim()
169+
.split(/\r?\n/)[0] // `where` may return multiple lines — take the first
170+
if (found) {
171+
candidates.push({ path: found, source: `${whichCmd} dbt (PATH)`, binDir: dirname(found) })
179172
}
180173
} catch {}
181174

182-
// 11. Common known locations (last resort)
183-
const home = process.env.HOME ?? ""
184-
const knownPaths = [
185-
{ path: join(home, ".local", "bin", "dbt"), source: "~/.local/bin/dbt (pipx/user pip)" },
186-
{ path: "/usr/local/bin/dbt", source: "/usr/local/bin/dbt (system pip)" },
187-
{ path: "/opt/homebrew/bin/dbt", source: "/opt/homebrew/bin/dbt (homebrew, deprecated)" },
188-
]
175+
// 9. Common known locations (last resort)
176+
const home = process.env.HOME ?? process.env.USERPROFILE ?? ""
177+
const knownPaths = isWindows
178+
? [
179+
{
180+
path: join(home, "AppData", "Roaming", "Python", "Scripts", "dbt.exe"),
181+
source: "%APPDATA%/Python/Scripts/dbt.exe (user pip)",
182+
},
183+
{
184+
path: join(home, "AppData", "Local", "Programs", "Python", "Scripts", "dbt.exe"),
185+
source: "%LOCALAPPDATA%/Programs/Python/Scripts/dbt.exe (system pip)",
186+
},
187+
]
188+
: [
189+
{ path: join(home, ".local", "bin", "dbt"), source: "~/.local/bin/dbt (pipx/user pip)" },
190+
{ path: "/usr/local/bin/dbt", source: "/usr/local/bin/dbt (system pip)" },
191+
{ path: "/opt/homebrew/bin/dbt", source: "/opt/homebrew/bin/dbt (homebrew, deprecated)" },
192+
]
189193
for (const kp of knownPaths) {
190194
candidates.push({ ...kp, binDir: dirname(kp.path) })
191195
}
@@ -197,8 +201,8 @@ export function resolveDbt(pythonPath?: string, projectRoot?: string): ResolvedD
197201
}
198202
}
199203

200-
// Nothing found — return bare "dbt" and hope PATH has it
201-
return { path: "dbt", source: "fallback (bare dbt on PATH)" }
204+
// Nothing found — return bare "dbt" (or "dbt.exe") and hope PATH has it
205+
return { path: `dbt${EXE}`, source: "fallback (bare dbt on PATH)" }
202206
}
203207

204208
/**

0 commit comments

Comments
 (0)