Skip to main content

elph_core/utils/
path.rs

1//! Generic home, data, and project directory resolution.
2
3use std::path::{Path, PathBuf};
4
5use anyhow::{Context, Result};
6
7/// Environment and naming knobs for an application's home directories.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub struct PathResolver {
10    /// Override env var for the config/home directory (e.g. `ELPH_HOME`).
11    pub home_env: &'static str,
12    /// Override env var for the data directory (e.g. `ELPH_DATA_DIR`).
13    pub data_env: &'static str,
14    /// Override env var for the project directory (e.g. `ELPH_PROJECT_DIR`).
15    pub project_env: &'static str,
16    /// Config directory name under `$HOME` (e.g. `.elph`).
17    pub config_dir_name: &'static str,
18    /// Data directory name under XDG data home (e.g. `elph`).
19    pub data_dir_name: &'static str,
20}
21
22/// Resolved config, data, and project directories.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct ResolvedPaths {
25    pub config_dir: PathBuf,
26    pub data_dir: PathBuf,
27    pub project_dir: PathBuf,
28}
29
30impl PathResolver {
31    pub fn resolve(&self) -> Result<ResolvedPaths> {
32        Ok(ResolvedPaths::from_dirs(
33            self.config_dir()?,
34            self.data_dir()?,
35            self.project_dir()?,
36        ))
37    }
38
39    fn config_dir(&self) -> Result<PathBuf> {
40        if let Some(path) = env_path(self.home_env) {
41            return Ok(path);
42        }
43
44        Ok(user_home()?.join(self.config_dir_name))
45    }
46
47    fn data_dir(&self) -> Result<PathBuf> {
48        if let Some(path) = env_path(self.data_env) {
49            return Ok(path);
50        }
51
52        if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
53            let trimmed = xdg.trim();
54            if !trimmed.is_empty() {
55                return Ok(Path::new(trimmed).join(self.data_dir_name));
56            }
57        }
58
59        Ok(user_home()?.join(".local").join("share").join(self.data_dir_name))
60    }
61
62    fn project_dir(&self) -> Result<PathBuf> {
63        if let Some(path) = env_path(self.project_env) {
64            return Ok(path);
65        }
66
67        std::env::current_dir().map_err(Into::into)
68    }
69}
70
71impl ResolvedPaths {
72    pub fn from_dirs(config_dir: PathBuf, data_dir: PathBuf, project_dir: PathBuf) -> Self {
73        Self {
74            config_dir,
75            data_dir,
76            project_dir,
77        }
78    }
79}
80
81/// Common config/data path helpers shared by Elph applications.
82pub trait AppPaths {
83    fn config_dir(&self) -> &PathBuf;
84    fn data_dir(&self) -> &PathBuf;
85
86    fn settings_path(&self) -> PathBuf {
87        self.config_dir().join("settings.json")
88    }
89
90    fn trust_path(&self) -> PathBuf {
91        self.config_dir().join("trust.json")
92    }
93
94    fn bundled_dir(&self) -> PathBuf {
95        self.config_dir().join("bundled")
96    }
97
98    fn bundled_manifest_path(&self) -> PathBuf {
99        self.bundled_dir().join("manifest.json")
100    }
101
102    fn prompts_dir(&self) -> PathBuf {
103        self.config_dir().join("prompts")
104    }
105
106    fn providers_dir(&self) -> PathBuf {
107        self.config_dir().join("providers")
108    }
109
110    fn sessions_dir(&self) -> PathBuf {
111        self.config_dir().join("sessions")
112    }
113
114    fn skills_dir(&self) -> PathBuf {
115        self.config_dir().join("skills")
116    }
117
118    fn worktrees_dir(&self) -> PathBuf {
119        self.config_dir().join("worktrees")
120    }
121
122    fn attachments_dir(&self) -> PathBuf {
123        self.data_dir().join("attachments")
124    }
125
126    fn downloads_dir(&self) -> PathBuf {
127        self.data_dir().join("downloads")
128    }
129
130    fn logs_dir(&self) -> PathBuf {
131        self.data_dir().join("logs")
132    }
133
134    fn vendor_dir(&self) -> PathBuf {
135        self.data_dir().join("vendor")
136    }
137
138    /// Local ONNX embedding model cache (fastembed / Hugging Face downloads).
139    fn models_dir(&self) -> PathBuf {
140        self.data_dir().join("models")
141    }
142
143    fn metadata_db_path(&self) -> PathBuf {
144        self.data_dir().join("metadata.db")
145    }
146
147    fn version_path(&self) -> PathBuf {
148        self.data_dir().join("version.json")
149    }
150
151    fn bundled_content_dirs(&self) -> [PathBuf; 4] {
152        let bundled = self.bundled_dir();
153        [
154            bundled.join("agents"),
155            bundled.join("personas"),
156            bundled.join("skills"),
157            bundled.join("user-guide"),
158        ]
159    }
160
161    fn standard_required_dirs(&self) -> Vec<PathBuf> {
162        let mut dirs = self.bundled_content_dirs().into_iter().collect::<Vec<_>>();
163        dirs.extend([
164            self.prompts_dir(),
165            self.providers_dir(),
166            self.sessions_dir(),
167            self.skills_dir(),
168            self.worktrees_dir(),
169            self.attachments_dir(),
170            self.downloads_dir(),
171            self.logs_dir(),
172            self.vendor_dir(),
173            self.models_dir(),
174        ]);
175        dirs
176    }
177}
178
179fn env_path(name: &str) -> Option<PathBuf> {
180    let value = std::env::var(name).ok()?;
181    let trimmed = value.trim();
182    if trimmed.is_empty() {
183        None
184    } else {
185        Some(PathBuf::from(trimmed))
186    }
187}
188
189fn user_home() -> Result<PathBuf> {
190    std::env::var_os("HOME").map(PathBuf::from).context("HOME is not set")
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    const TEST_RESOLVER: PathResolver = PathResolver {
198        home_env: "TEST_AGENT_HOME",
199        data_env: "TEST_AGENT_DATA",
200        project_env: "TEST_AGENT_PROJECT",
201        config_dir_name: ".test-agent",
202        data_dir_name: "test-agent",
203    };
204
205    struct TestPaths {
206        config_dir: PathBuf,
207        data_dir: PathBuf,
208    }
209
210    impl AppPaths for TestPaths {
211        fn config_dir(&self) -> &PathBuf {
212            &self.config_dir
213        }
214
215        fn data_dir(&self) -> &PathBuf {
216            &self.data_dir
217        }
218    }
219
220    #[test]
221    fn resolves_from_explicit_dirs() {
222        let paths = ResolvedPaths::from_dirs(PathBuf::from("/cfg"), PathBuf::from("/data"), PathBuf::from("/repo"));
223
224        assert_eq!(paths.config_dir, PathBuf::from("/cfg"));
225        assert_eq!(paths.data_dir, PathBuf::from("/data"));
226        assert_eq!(paths.project_dir, PathBuf::from("/repo"));
227    }
228
229    #[test]
230    fn resolver_exposes_static_names() {
231        assert_eq!(TEST_RESOLVER.config_dir_name, ".test-agent");
232        assert_eq!(TEST_RESOLVER.data_dir_name, "test-agent");
233    }
234
235    #[test]
236    fn app_paths_builds_expected_file_paths() {
237        let paths = TestPaths {
238            config_dir: PathBuf::from("/cfg"),
239            data_dir: PathBuf::from("/data"),
240        };
241
242        assert_eq!(paths.metadata_db_path(), PathBuf::from("/data/metadata.db"));
243        assert_eq!(
244            paths.bundled_manifest_path(),
245            PathBuf::from("/cfg/bundled/manifest.json")
246        );
247        assert_eq!(paths.standard_required_dirs().len(), 14);
248    }
249}