use std::path::{Path, PathBuf};
use std::sync::OnceLock;
pub fn find_workspace_env_local(start: &Path) -> Option<PathBuf> {
let mut dir = Some(start.to_path_buf());
while let Some(d) = dir {
let candidate = d.join(".env.local");
if candidate.is_file() {
return Some(candidate);
}
dir = d.parent().map(Path::to_path_buf);
}
None
}
pub fn load_env_from_path(path: &Path) -> bool {
dotenvy::from_path(path).is_ok()
}
pub fn load_env_local_once() {
static LOADED: OnceLock<()> = OnceLock::new();
LOADED.get_or_init(|| {
if let Ok(cwd) = std::env::current_dir()
&& let Some(path) = find_workspace_env_local(&cwd)
{
let _ = dotenvy::from_path(&path);
}
});
}
pub fn read_var_from_env_local(path: &Path, var: &str) -> Option<String> {
let iter = dotenvy::from_path_iter(path).ok()?;
iter.flatten()
.find(|(k, _)| k == var)
.map(|(_, v)| v)
.filter(|v| !v.is_empty())
}
pub fn env_local_value(var: &str) -> Option<String> {
let cwd = std::env::current_dir().ok()?;
let path = find_workspace_env_local(&cwd)?;
read_var_from_env_local(&path, var)
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
#[test]
fn finds_env_local_in_ancestor() {
let tmp = tempfile::TempDir::new().unwrap();
let child = tmp.path().join("a").join("b");
std::fs::create_dir_all(&child).unwrap();
std::fs::write(tmp.path().join(".env.local"), "X=1\n").unwrap();
let found = find_workspace_env_local(&child).unwrap();
assert_eq!(found, tmp.path().join(".env.local"));
}
#[test]
fn absent_env_local_returns_none() {
let tmp = tempfile::TempDir::new().unwrap();
assert_eq!(find_workspace_env_local(tmp.path()), None);
}
#[test]
fn read_var_from_env_local_finds_value() {
let tmp = tempfile::TempDir::new().unwrap();
let path = tmp.path().join(".env.local");
std::fs::write(&path, "OPENAI_API_KEY=from-dotenv\nEMPTY=\n").unwrap();
assert_eq!(
read_var_from_env_local(&path, "OPENAI_API_KEY"),
Some("from-dotenv".to_string())
);
assert!(std::env::var("OPENAI_API_KEY").is_err());
}
#[test]
fn read_var_from_env_local_absent_is_none() {
let tmp = tempfile::TempDir::new().unwrap();
let path = tmp.path().join(".env.local");
std::fs::write(&path, "EMPTY=\n").unwrap();
assert_eq!(read_var_from_env_local(&path, "EMPTY"), None);
assert_eq!(read_var_from_env_local(&path, "MISSING"), None);
}
#[test]
#[serial(dotenv_credential_env)]
fn load_env_from_path_sets_new_var() {
let var = "TRUSTY_TEST_DOTENV_NEW_VAR";
unsafe {
std::env::remove_var(var);
}
let tmp = tempfile::TempDir::new().unwrap();
let env_path = tmp.path().join(".env.local");
std::fs::write(&env_path, format!("{var}=from-dotenv\n")).unwrap();
assert!(load_env_from_path(&env_path));
assert_eq!(std::env::var(var).unwrap(), "from-dotenv");
unsafe {
std::env::remove_var(var);
}
}
#[test]
#[serial(dotenv_credential_env)]
fn load_env_from_path_does_not_override_existing() {
let var = "TRUSTY_TEST_DOTENV_EXISTING_VAR";
unsafe {
std::env::set_var(var, "already-set");
}
let tmp = tempfile::TempDir::new().unwrap();
let env_path = tmp.path().join(".env.local");
std::fs::write(&env_path, format!("{var}=from-dotenv\n")).unwrap();
assert!(load_env_from_path(&env_path));
assert_eq!(std::env::var(var).unwrap(), "already-set");
unsafe {
std::env::remove_var(var);
}
}
}