Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 4 additions & 57 deletions packages/dbt-tools/src/commands/init.ts
Original file line number Diff line number Diff line change
@@ -1,74 +1,21 @@
import { join, resolve } from "path"
import { resolve, join } from "path"
import { existsSync } from "fs"
import { execFileSync } from "child_process"
import { write, type Config } from "../config"
import { write, findProjectRoot, discoverPython, type Config } from "../config"
import { all } from "../check"

function find(start: string): string | null {
let dir = resolve(start)
while (true) {
if (existsSync(join(dir, "dbt_project.yml"))) return dir
const parent = resolve(dir, "..")
if (parent === dir) return null
dir = parent
}
}

/**
* Discover the Python binary, checking multiple environment managers.
*
* Priority:
* 1. Project-local .venv/bin/python (uv, pdm, venv, poetry in-project)
* 2. VIRTUAL_ENV/bin/python (activated venv)
* 3. CONDA_PREFIX/bin/python (conda)
* 4. `which python3` / `which python` (system PATH)
* 5. Fallback "python3" (hope for the best)
*/
function python(projectRoot?: string): string {
// Check project-local venvs first (most reliable for dbt projects)
if (projectRoot) {
for (const venvDir of [".venv", "venv", "env"]) {
const py = join(projectRoot, venvDir, "bin", "python")
if (existsSync(py)) return py
}
}

// Check VIRTUAL_ENV (set by activate scripts)
const virtualEnv = process.env.VIRTUAL_ENV
if (virtualEnv) {
const py = join(virtualEnv, "bin", "python")
if (existsSync(py)) return py
}

// Check CONDA_PREFIX (set by conda activate)
const condaPrefix = process.env.CONDA_PREFIX
if (condaPrefix) {
const py = join(condaPrefix, "bin", "python")
if (existsSync(py)) return py
}

// Fall back to PATH-based discovery
for (const cmd of ["python3", "python"]) {
try {
return execFileSync("which", [cmd], { encoding: "utf-8" }).trim()
} catch {}
}
return "python3"
}

export async function init(args: string[]) {
const idx = args.indexOf("--project-root")
const root = idx >= 0 ? args[idx + 1] : undefined
const pidx = args.indexOf("--python-path")
const py = pidx >= 0 ? args[pidx + 1] : undefined

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

const cfg: Config = {
projectRoot: project,
pythonPath: py ?? python(project),
pythonPath: py ?? discoverPython(project),
dbtIntegration: "corecommand",
queryLimit: 500,
}
Expand Down
90 changes: 86 additions & 4 deletions packages/dbt-tools/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { homedir } from "os"
import { join } from "path"
import { join, resolve } from "path"
import { readFile, writeFile, mkdir } from "fs/promises"
import { existsSync } from "fs"
import { execFileSync } from "child_process"

type Config = {
projectRoot: string
Expand All @@ -18,11 +19,92 @@ function configPath() {
return join(configDir(), "dbt.json")
}

/**
* Walk up from `start` to find the nearest directory containing dbt_project.yml.
* Returns null if none found.
*/
export function findProjectRoot(start = process.cwd()): string | null {
let dir = resolve(start)
while (true) {
if (existsSync(join(dir, "dbt_project.yml"))) return dir
const parent = resolve(dir, "..")
if (parent === dir) return null
dir = parent
}
}

const isWindows = process.platform === "win32"
// Windows venvs use Scripts/, Unix venvs use bin/
const VENV_BIN = isWindows ? "Scripts" : "bin"
// Windows executables have .exe suffix
const EXE = isWindows ? ".exe" : ""

/**
* Discover the Python binary for a given project root.
* Priority: project-local .venv → VIRTUAL_ENV → CONDA_PREFIX → which/where python
*/
export function discoverPython(projectRoot: string): string {
// Candidate Python binary names (python3 first on Unix; python.exe on Windows)
const pythonBins = isWindows ? ["python.exe", "python3.exe"] : ["python3", "python"]

// Project-local venvs (uv, pdm, venv, poetry in-project, rye)
for (const venvDir of [".venv", "venv", "env"]) {
for (const bin of pythonBins) {
const py = join(projectRoot, venvDir, VENV_BIN, bin)
if (existsSync(py)) return py
}
}

// VIRTUAL_ENV (set by activate scripts)
const virtualEnv = process.env.VIRTUAL_ENV
if (virtualEnv) {
for (const bin of pythonBins) {
const py = join(virtualEnv, VENV_BIN, bin)
if (existsSync(py)) return py
}
}

// CONDA_PREFIX (Conda places python at env root on Windows, bin/ on Unix)
const condaPrefix = process.env.CONDA_PREFIX
if (condaPrefix) {
for (const bin of pythonBins) {
const py = isWindows ? join(condaPrefix, bin) : join(condaPrefix, VENV_BIN, bin)
if (existsSync(py)) return py
}
}

// PATH-based discovery (`where` on Windows, `which` on Unix)
const whichCmd = isWindows ? "where" : "which"
const cmds = isWindows ? ["python.exe", "python3.exe", "python"] : ["python3", "python"]
for (const cmd of cmds) {
try {
// `where` on Windows may return multiple lines — take the first
const first = execFileSync(whichCmd, [cmd], { encoding: "utf-8", timeout: 5_000 }).trim().split(/\r?\n/)[0]
if (first) return first
} catch {}
}
return isWindows ? "python.exe" : "python3"
}

async function read(): Promise<Config | null> {
const p = configPath()
if (!existsSync(p)) return null
const raw = await readFile(p, "utf-8")
return JSON.parse(raw) as Config
if (existsSync(p)) {
try {
const raw = await readFile(p, "utf-8")
return JSON.parse(raw) as Config
} catch {
Comment on lines +94 to +95
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate parsed config shape before returning it.

JSON.parse success does not guarantee a valid Config. A malformed-but-valid object (missing projectRoot, wrong dbtIntegration, etc.) will propagate and can break downstream consumers.

Suggested patch
 type Config = {
   projectRoot: string
   pythonPath: string
   dbtIntegration: string
   queryLimit: number
 }
+
+function isConfig(value: unknown): value is Config {
+  if (!value || typeof value !== "object") return false
+  const v = value as Record<string, unknown>
+  return (
+    typeof v.projectRoot === "string" &&
+    typeof v.pythonPath === "string" &&
+    typeof v.dbtIntegration === "string" &&
+    typeof v.queryLimit === "number"
+  )
+}

 async function read(): Promise<Config | null> {
   const p = configPath()
   if (existsSync(p)) {
     try {
       const raw = await readFile(p, "utf-8")
-      return JSON.parse(raw) as Config
+      const parsed: unknown = JSON.parse(raw)
+      if (isConfig(parsed)) return parsed
     } catch {
       // Malformed config — fall through to auto-discovery
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/dbt-tools/src/config.ts` around lines 94 - 95, The code currently
returns JSON.parse(raw) as Config without runtime validation; after parsing the
raw string, validate the resulting object’s shape (required fields like
projectRoot and dbtIntegration and their expected types/enum values) before
returning it from the function that contains "JSON.parse(raw) as Config"; if
validation fails, throw a clear error in the catch/validation path instead of
returning the unchecked object. Use a runtime schema validator (e.g., zod) or
explicit property/type checks against the Config interface, and replace the
blind cast with the validated Config instance so downstream consumers receive a
correct shape.

// Malformed config — fall through to auto-discovery
}
}
// No config file — auto-discover from cwd so `altimate-dbt init` isn't required
const projectRoot = findProjectRoot()
if (!projectRoot) return null
return {
projectRoot,
pythonPath: discoverPython(projectRoot),
dbtIntegration: "corecommand",
queryLimit: 500,
}
}

async function write(cfg: Config) {
Expand Down
Loading
Loading