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());
let mut worktree_main_root: Option<PathBuf> = None;
while let Some(d) = dir {
let candidate = d.join(".env.local");
if candidate.is_file() {
return Some(candidate);
}
if worktree_main_root
.as_deref()
.is_some_and(|root| canonical(&d) == root)
{
return None;
}
let git_marker = d.join(".git");
if git_marker.is_dir() {
return None;
}
if git_marker.is_file() {
match resolve_worktree_main_root(&git_marker) {
Some(root)
if d.ancestors()
.skip(1)
.any(|a| canonical(a) == canonical(&root)) =>
{
worktree_main_root = Some(canonical(&root));
}
_ => return None,
}
} else if is_workspace_cargo_toml(&d) {
return None;
}
dir = d.parent().map(Path::to_path_buf);
}
None
}
fn canonical(path: &Path) -> PathBuf {
path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
}
fn is_workspace_cargo_toml(dir: &Path) -> bool {
std::fs::read_to_string(dir.join("Cargo.toml"))
.is_ok_and(|contents| contents.contains("[workspace]"))
}
fn resolve_worktree_main_root(worktree_git_file: &Path) -> Option<PathBuf> {
let contents = std::fs::read_to_string(worktree_git_file).ok()?;
let gitdir = contents
.lines()
.find_map(|line| line.strip_prefix("gitdir:"))?;
let gitdir = gitdir.trim();
if gitdir.is_empty() {
return None;
}
let gitdir_path = PathBuf::from(gitdir);
let gitdir_path = if gitdir_path.is_absolute() {
gitdir_path
} else {
worktree_git_file.parent()?.join(gitdir_path)
};
let worktrees_dir = gitdir_path.parent()?;
if worktrees_dir.file_name() != Some(std::ffi::OsStr::new("worktrees")) {
return None;
}
let commondir = worktrees_dir.parent()?;
commondir.parent().map(Path::to_path_buf)
}
pub fn load_env_from_path(path: &Path) -> bool {
dotenvy::from_path(path).is_ok()
}
pub fn user_env_local_path(home: &Path) -> Option<PathBuf> {
let candidate = home.join(".env.local");
candidate.is_file().then_some(candidate)
}
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);
}
if let Some(home) = dirs::home_dir()
&& let Some(path) = user_env_local_path(&home)
{
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 walk_stops_at_git_dir_boundary() {
let tmp = tempfile::TempDir::new().unwrap();
std::fs::write(tmp.path().join(".env.local"), "OUTER=1\n").unwrap();
let repo = tmp.path().join("repo");
std::fs::create_dir_all(repo.join(".git")).unwrap();
let nested = repo.join("nested").join("deeper");
std::fs::create_dir_all(&nested).unwrap();
assert_eq!(find_workspace_env_local(&nested), None);
}
#[test]
fn walk_stops_at_git_file_boundary() {
let tmp = tempfile::TempDir::new().unwrap();
std::fs::write(tmp.path().join(".env.local"), "OUTER=1\n").unwrap();
let repo = tmp.path().join("worktree");
std::fs::create_dir_all(&repo).unwrap();
std::fs::write(repo.join(".git"), "gitdir: /elsewhere/.git/worktrees/x\n").unwrap();
let nested = repo.join("nested");
std::fs::create_dir_all(&nested).unwrap();
assert_eq!(find_workspace_env_local(&nested), None);
}
#[test]
fn resolves_main_checkout_root_from_worktree_pointer() {
let tmp = tempfile::TempDir::new().unwrap();
let main_root = tmp.path().join("main-repo");
let gitdir = main_root.join(".git").join("worktrees").join("wt");
std::fs::create_dir_all(&gitdir).unwrap();
let worktree_git_file = tmp.path().join("worktree-git-file");
std::fs::write(
&worktree_git_file,
format!("gitdir: {}\n", gitdir.display()),
)
.unwrap();
assert_eq!(
resolve_worktree_main_root(&worktree_git_file),
Some(main_root)
);
}
#[test]
fn resolves_main_checkout_root_with_non_dot_git_commondir_name() {
let tmp = tempfile::TempDir::new().unwrap();
let main_root = tmp.path().join("trusty-tools");
let gitdir = main_root.join(".base").join("worktrees").join("session-x");
std::fs::create_dir_all(&gitdir).unwrap();
let worktree_git_file = tmp.path().join("worktree-git-file");
std::fs::write(
&worktree_git_file,
format!("gitdir: {}\n", gitdir.display()),
)
.unwrap();
assert_eq!(
resolve_worktree_main_root(&worktree_git_file),
Some(main_root)
);
}
#[test]
fn malformed_pointer_file_returns_none() {
let tmp = tempfile::TempDir::new().unwrap();
let bogus = tmp.path().join("bogus-git-file");
std::fs::write(&bogus, "not a gitdir pointer at all\n").unwrap();
assert_eq!(resolve_worktree_main_root(&bogus), None);
let missing = tmp.path().join("does-not-exist");
assert_eq!(resolve_worktree_main_root(&missing), None);
}
#[test]
fn worktree_pointer_reaches_env_local_in_main_checkout_root() {
let tmp = tempfile::TempDir::new().unwrap();
let main_root = tmp.path().join("main-repo");
std::fs::create_dir_all(main_root.join(".git").join("worktrees").join("wt")).unwrap();
std::fs::write(main_root.join(".env.local"), "REAL=1\n").unwrap();
let worktree_root = main_root.join(".claude").join("worktrees").join("wt");
std::fs::create_dir_all(&worktree_root).unwrap();
let gitdir = main_root.join(".git").join("worktrees").join("wt");
std::fs::write(
worktree_root.join(".git"),
format!("gitdir: {}\n", gitdir.display()),
)
.unwrap();
let nested = worktree_root
.join("crates")
.join("trusty-common")
.join("src");
std::fs::create_dir_all(&nested).unwrap();
let found = find_workspace_env_local(&nested).unwrap();
assert_eq!(found, main_root.join(".env.local"));
}
#[test]
fn worktree_pointer_with_base_commondir_reaches_env_local_in_main_checkout_root() {
let tmp = tempfile::TempDir::new().unwrap();
let main_root = tmp.path().join("trusty-tools");
std::fs::create_dir_all(main_root.join(".base").join("worktrees").join("session-x"))
.unwrap();
std::fs::write(main_root.join(".env.local"), "REAL=1\n").unwrap();
let worktree_root = main_root
.join(".base")
.join(".claude")
.join("worktrees")
.join("session-x");
std::fs::create_dir_all(&worktree_root).unwrap();
let gitdir = main_root.join(".base").join("worktrees").join("session-x");
std::fs::write(
worktree_root.join(".git"),
format!("gitdir: {}\n", gitdir.display()),
)
.unwrap();
let nested = worktree_root
.join("crates")
.join("trusty-common")
.join("src");
std::fs::create_dir_all(&nested).unwrap();
let found = find_workspace_env_local(&nested).unwrap();
assert_eq!(found, main_root.join(".env.local"));
}
#[test]
fn real_git_worktree_add_reaches_main_checkout_env_local() {
let tmp = tempfile::TempDir::new().unwrap();
let main_repo = tmp.path().join("main");
std::fs::create_dir_all(&main_repo).unwrap();
let run = |args: &[&str]| {
let status = std::process::Command::new("git")
.args(args)
.current_dir(&main_repo)
.env("GIT_AUTHOR_NAME", "trusty-tools-test")
.env("GIT_AUTHOR_EMAIL", "test@example.invalid")
.env("GIT_COMMITTER_NAME", "trusty-tools-test")
.env("GIT_COMMITTER_EMAIL", "test@example.invalid")
.status()
.expect("git must be on PATH to run this test");
assert!(status.success(), "git {args:?} failed");
};
run(&["init", "-q", "-b", "main"]);
std::fs::write(main_repo.join("README.md"), "fixture\n").unwrap();
run(&["add", "README.md"]);
run(&["commit", "-q", "-m", "init"]);
std::fs::write(
main_repo.join(".env.local"),
"FIREWORKS_API_KEY=real-secret-value\n", )
.unwrap();
let worktree_path = main_repo.join(".claude").join("worktrees").join("fixture");
run(&[
"worktree",
"add",
"-q",
"-b",
"fixture-branch",
worktree_path.to_str().unwrap(),
]);
let nested = worktree_path.join("crates").join("trusty-common");
std::fs::create_dir_all(&nested).unwrap();
let found = find_workspace_env_local(&nested)
.expect("main checkout .env.local must be reachable from a real linked worktree");
assert_eq!(found, main_repo.join(".env.local"));
}
#[test]
fn real_git_worktree_add_with_renamed_base_commondir_reaches_env_local() {
let tmp = tempfile::TempDir::new().unwrap();
let main_repo = tmp.path().join("main");
std::fs::create_dir_all(&main_repo).unwrap();
let git_dir = main_repo.join(".base");
let run = |args: &[&str]| {
let status = std::process::Command::new("git")
.arg(format!("--git-dir={}", git_dir.display()))
.arg(format!("--work-tree={}", main_repo.display()))
.args(args)
.env("GIT_AUTHOR_NAME", "trusty-tools-test")
.env("GIT_AUTHOR_EMAIL", "test@example.invalid")
.env("GIT_COMMITTER_NAME", "trusty-tools-test")
.env("GIT_COMMITTER_EMAIL", "test@example.invalid")
.status()
.expect("git must be on PATH to run this test");
assert!(status.success(), "git {args:?} failed");
};
run(&["init", "-q", "-b", "main"]);
assert!(
!main_repo.join(".git").exists(),
"this test's whole point is that NO `.git` exists — only `.base`"
);
std::fs::write(main_repo.join("README.md"), "fixture\n").unwrap();
run(&["add", "README.md"]);
run(&["commit", "-q", "-m", "init"]);
std::fs::write(
main_repo.join(".env.local"),
"FIREWORKS_API_KEY=real-secret-value\n", )
.unwrap();
let worktree_path = main_repo
.join(".base")
.join(".claude")
.join("worktrees")
.join("fixture");
run(&[
"worktree",
"add",
"-q",
"-b",
"fixture-branch",
worktree_path.to_str().unwrap(),
]);
let nested = worktree_path.join("crates").join("trusty-common");
std::fs::create_dir_all(&nested).unwrap();
let found = find_workspace_env_local(&nested).expect(
"main checkout .env.local must be reachable from a real .base-commondir worktree",
);
assert_eq!(found, main_repo.join(".env.local"));
}
#[test]
fn walk_finds_env_local_at_the_boundary_itself() {
let tmp = tempfile::TempDir::new().unwrap();
let repo = tmp.path().join("repo");
std::fs::create_dir_all(repo.join(".git")).unwrap();
std::fs::write(repo.join(".env.local"), "X=1\n").unwrap();
let nested = repo.join("nested");
std::fs::create_dir_all(&nested).unwrap();
let found = find_workspace_env_local(&nested).unwrap();
assert_eq!(found, repo.join(".env.local"));
}
#[test]
fn workspace_cargo_toml_is_a_boundary() {
let tmp = tempfile::TempDir::new().unwrap();
std::fs::write(tmp.path().join(".env.local"), "OUTER=1\n").unwrap();
let repo = tmp.path().join("repo");
std::fs::create_dir_all(&repo).unwrap();
std::fs::write(
repo.join("Cargo.toml"),
"[workspace]\nmembers = [\"crates/*\"]\n",
)
.unwrap();
let nested = repo.join("nested");
std::fs::create_dir_all(&nested).unwrap();
assert_eq!(find_workspace_env_local(&nested), None);
}
#[test]
fn plain_package_cargo_toml_is_not_a_boundary() {
let tmp = tempfile::TempDir::new().unwrap();
std::fs::write(tmp.path().join(".env.local"), "OUTER=1\n").unwrap();
let repo = tmp.path().join("repo");
std::fs::create_dir_all(&repo).unwrap();
std::fs::write(repo.join("Cargo.toml"), "[package]\nname = \"x\"\n").unwrap();
let nested = repo.join("nested");
std::fs::create_dir_all(&nested).unwrap();
let found = find_workspace_env_local(&nested).unwrap();
assert_eq!(found, tmp.path().join(".env.local"));
}
#[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);
}
}
#[test]
fn user_env_local_path_finds_file() {
let tmp = tempfile::TempDir::new().unwrap();
std::fs::write(tmp.path().join(".env.local"), "X=1\n").unwrap();
assert_eq!(
user_env_local_path(tmp.path()),
Some(tmp.path().join(".env.local"))
);
}
#[test]
fn user_env_local_path_absent_is_none() {
let tmp = tempfile::TempDir::new().unwrap();
assert_eq!(user_env_local_path(tmp.path()), None);
}
#[test]
#[serial(dotenv_credential_env)]
fn project_env_local_beats_user_env_local() {
unsafe {
std::env::remove_var("OPENROUTER_API_KEY");
}
let project_tmp = tempfile::TempDir::new().unwrap();
let project_env = project_tmp.path().join(".env.local");
std::fs::write(&project_env, "OPENROUTER_API_KEY=from-project\n").unwrap();
let user_tmp = tempfile::TempDir::new().unwrap();
let user_env = user_env_local_path(user_tmp.path());
assert_eq!(user_env, None);
let user_env_path = user_tmp.path().join(".env.local");
std::fs::write(&user_env_path, "OPENROUTER_API_KEY=from-user\n").unwrap();
assert_eq!(
user_env_local_path(user_tmp.path()),
Some(user_env_path.clone())
);
assert!(load_env_from_path(&project_env));
assert!(load_env_from_path(&user_env_path));
assert_eq!(std::env::var("OPENROUTER_API_KEY").unwrap(), "from-project");
unsafe {
std::env::remove_var("OPENROUTER_API_KEY");
}
}
#[test]
#[serial(dotenv_credential_env)]
fn user_env_local_supplies_value_when_no_project_tier() {
unsafe {
std::env::remove_var("OPENROUTER_API_KEY");
}
let user_tmp = tempfile::TempDir::new().unwrap();
let user_env_path = user_tmp.path().join(".env.local");
std::fs::write(&user_env_path, "OPENROUTER_API_KEY=from-user\n").unwrap();
assert!(load_env_from_path(&user_env_path));
assert_eq!(std::env::var("OPENROUTER_API_KEY").unwrap(), "from-user");
unsafe {
std::env::remove_var("OPENROUTER_API_KEY");
}
}
}