package config import ( "bufio" "os" "path/filepath" "strings" ) // loadDotEnv loads the monorepo-root .env into the process environment. // Existing variables (including empty ones set by tests) are never overridden. // Missing file is a no-op — production typically injects env without a file. func loadDotEnv() { if path := strings.TrimSpace(os.Getenv("DOTENV_PATH")); path != "" { _ = applyEnvFile(path) return } if root, ok := findMonorepoRoot(); ok { _ = applyEnvFile(filepath.Join(root, ".env")) } } func findMonorepoRoot() (string, bool) { cwd, err := os.Getwd() if err != nil { return "", false } dir := cwd for { if isMonorepoRoot(dir) { return dir, true } parent := filepath.Dir(dir) if parent == dir { return "", false } dir = parent } } func isMonorepoRoot(dir string) bool { api := filepath.Join(dir, "apps", "api") web := filepath.Join(dir, "apps", "web") if st, err := os.Stat(api); err != nil || !st.IsDir() { return false } if st, err := os.Stat(web); err != nil || !st.IsDir() { return false } // Prefer package.json workspaces marker when present. if _, err := os.Stat(filepath.Join(dir, "package.json")); err == nil { return true } return true } func applyEnvFile(path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() sc := bufio.NewScanner(f) // Allow long values (keys, DSNs). sc.Buffer(make([]byte, 0, 64*1024), 1024*1024) for sc.Scan() { line := strings.TrimSpace(sc.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } if strings.HasPrefix(line, "export ") { line = strings.TrimSpace(strings.TrimPrefix(line, "export ")) } key, val, ok := strings.Cut(line, "=") if !ok { continue } key = strings.TrimSpace(key) if key == "" { continue } if _, exists := os.LookupEnv(key); exists { continue } val = strings.TrimSpace(val) if len(val) >= 2 { if (val[0] == '"' && val[len(val)-1] == '"') || (val[0] == '\'' && val[len(val)-1] == '\'') { val = val[1 : len(val)-1] } } _ = os.Setenv(key, val) } return sc.Err() }