@@ -29,6 +29,12 @@ import { execFileSync } from "child_process"
2929import { existsSync , realpathSync , readFileSync } from "fs"
3030import { 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+
3238export 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