Skip to main content

agent_docs/
env.rs

1use std::env;
2use std::path::{Path, PathBuf};
3
4use anyhow::{Context, Result, bail};
5use nils_common::git as shared_git;
6
7use crate::paths::normalize_root_path;
8
9#[derive(Debug, Clone, Default)]
10pub struct PathOverrides {
11    pub agent_home: Option<PathBuf>,
12    pub project_path: Option<PathBuf>,
13}
14
15#[derive(Debug, Clone)]
16pub struct ResolvedRoots {
17    pub agent_home: PathBuf,
18    pub project_path: PathBuf,
19    pub is_linked_worktree: bool,
20    pub git_common_dir: Option<PathBuf>,
21    pub primary_worktree_path: Option<PathBuf>,
22}
23
24pub fn resolve_roots(overrides: &PathOverrides) -> Result<ResolvedRoots> {
25    let cwd = env::current_dir().context("failed to read current directory")?;
26    let agent_home = resolve_agent_home(overrides.agent_home.as_deref(), &cwd)?;
27    let project_path = resolve_project_path(overrides.project_path.as_deref(), &cwd);
28    let metadata = resolve_linked_worktree_metadata(&project_path);
29
30    Ok(ResolvedRoots {
31        agent_home,
32        project_path,
33        is_linked_worktree: metadata.is_linked_worktree,
34        git_common_dir: metadata.git_common_dir,
35        primary_worktree_path: metadata.primary_worktree_path,
36    })
37}
38
39fn resolve_agent_home(cli_value: Option<&Path>, cwd: &Path) -> Result<PathBuf> {
40    if let Some(path) = cli_value {
41        return Ok(normalize_root_path(path, cwd));
42    }
43
44    if let Some(path) = read_env_path("AGENT_HOME") {
45        return Ok(normalize_root_path(&path, cwd));
46    }
47
48    bail!("AGENT_HOME is required; set AGENT_HOME or pass --agent-home")
49}
50
51fn resolve_project_path(cli_value: Option<&Path>, cwd: &Path) -> PathBuf {
52    if let Some(path) = cli_value {
53        return normalize_root_path(path, cwd);
54    }
55
56    if let Some(path) = read_env_path("PROJECT_PATH") {
57        return normalize_root_path(&path, cwd);
58    }
59
60    if let Some(path) = git_top_level(cwd) {
61        return normalize_root_path(&path, cwd);
62    }
63
64    normalize_root_path(cwd, cwd)
65}
66
67fn read_env_path(name: &str) -> Option<PathBuf> {
68    let raw = env::var_os(name)?;
69    if raw.is_empty() {
70        None
71    } else {
72        Some(PathBuf::from(raw))
73    }
74}
75
76fn git_top_level(cwd: &Path) -> Option<PathBuf> {
77    git_rev_parse_path(cwd, "--show-toplevel")
78}
79
80#[derive(Debug, Default)]
81struct LinkedWorktreeMetadata {
82    is_linked_worktree: bool,
83    git_common_dir: Option<PathBuf>,
84    primary_worktree_path: Option<PathBuf>,
85}
86
87fn resolve_linked_worktree_metadata(cwd: &Path) -> LinkedWorktreeMetadata {
88    let absolute_git_dir = git_rev_parse_path(cwd, "--absolute-git-dir");
89    let git_common_dir = git_rev_parse_path(cwd, "--git-common-dir");
90
91    let Some(git_common_dir) = git_common_dir else {
92        return LinkedWorktreeMetadata::default();
93    };
94
95    let is_linked_worktree = absolute_git_dir
96        .as_ref()
97        .is_some_and(|git_dir| git_dir != &git_common_dir);
98    let primary_worktree_path = if is_linked_worktree {
99        git_common_dir.parent().map(Path::to_path_buf)
100    } else {
101        None
102    };
103
104    LinkedWorktreeMetadata {
105        is_linked_worktree,
106        git_common_dir: Some(git_common_dir),
107        primary_worktree_path,
108    }
109}
110
111fn git_rev_parse_path(cwd: &Path, arg: &str) -> Option<PathBuf> {
112    let raw = shared_git::rev_parse_in(cwd, &[arg]).ok().flatten()?;
113    Some(normalize_root_path(Path::new(&raw), cwd))
114}