11import { homedir } from "os"
2- import { join } from "path"
2+ import { join , resolve } from "path"
33import { readFile , writeFile , mkdir } from "fs/promises"
44import { existsSync } from "fs"
5+ import { execFileSync } from "child_process"
56
67type 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+
2189async 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
28110async function write ( cfg : Config ) {
0 commit comments