1use std::path::{Path, PathBuf};
17
18use crate::brand::{BRAND, Brand, env_name};
19use crate::env;
20
21#[derive(Clone, Debug, Default)]
24pub struct LocatorOverrides {
25 pub home: Option<PathBuf>,
27 pub cwd: Option<PathBuf>,
30}
31
32#[derive(Clone, Debug)]
37pub struct Locator {
38 home: PathBuf,
39 cwd: PathBuf,
40 brand: Brand,
41}
42
43impl Locator {
44 pub fn new(overrides: LocatorOverrides) -> Self {
47 Self::with_brand(overrides, BRAND)
48 }
49
50 pub fn with_brand(overrides: LocatorOverrides, brand: Brand) -> Self {
53 let home = Self::resolve_home(overrides.home, &brand);
54 let cwd = overrides
55 .cwd
56 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
57 Self { home, cwd, brand }
58 }
59
60 fn resolve_home(override_home: Option<PathBuf>, brand: &Brand) -> PathBuf {
63 if let Some(h) = override_home
64 && !h.as_os_str().is_empty()
65 {
66 return h;
67 }
68 let _ = brand; if let Some(from_env) = env::read_env("HOME") {
71 return PathBuf::from(from_env);
72 }
73 env::home_dir().unwrap_or_else(|| PathBuf::from("."))
74 }
75
76 pub fn home(&self) -> &Path {
78 &self.home
79 }
80
81 pub fn cwd(&self) -> &Path {
83 &self.cwd
84 }
85
86 pub fn profile_dir(&self) -> PathBuf {
90 self.home.join(self.brand.profile_dir_name)
91 }
92
93 pub fn settings_path(&self) -> PathBuf {
97 self.profile_dir().join(self.brand.settings_file_name)
98 }
99
100 pub fn project_settings_path(&self, cwd: Option<&Path>) -> PathBuf {
103 let root = match cwd {
104 Some(c) if c.is_absolute() => c.to_path_buf(),
105 Some(c) => self.cwd.join(c),
106 None => self.cwd.clone(),
107 };
108 root.join(self.brand.project_dir_name)
109 .join(self.brand.project_settings_file_name)
110 }
111
112 pub fn sessions_dir(&self) -> PathBuf {
116 self.profile_dir().join(self.brand.sessions_dir_name)
117 }
118
119 pub fn auth_store_path(&self) -> PathBuf {
121 self.profile_dir().join(self.brand.auth_store_file_name)
122 }
123
124 pub fn logs_dir(&self) -> PathBuf {
126 self.profile_dir().join(self.brand.logs_dir_name)
127 }
128
129 pub async fn ensure_profile_dir(&self) -> std::io::Result<PathBuf> {
133 self.ensure_dir(self.profile_dir()).await
134 }
135
136 pub async fn ensure_sessions_dir(&self) -> std::io::Result<PathBuf> {
138 self.ensure_dir(self.sessions_dir()).await
139 }
140
141 pub async fn ensure_logs_dir(&self) -> std::io::Result<PathBuf> {
143 self.ensure_dir(self.logs_dir()).await
144 }
145
146 pub async fn ensure_parent_of(&self, file_path: PathBuf) -> std::io::Result<PathBuf> {
149 if let Some(parent) = file_path.parent() {
150 self.ensure_dir(parent.to_path_buf()).await?;
151 }
152 Ok(file_path)
153 }
154
155 pub async fn ensure_dir(&self, dir: PathBuf) -> std::io::Result<PathBuf> {
157 tokio::fs::create_dir_all(&dir).await?;
158 Ok(dir)
159 }
160}
161
162pub fn home_env_var() -> String {
165 env_name("HOME")
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 fn sandbox(home: &str, cwd: &str) -> Locator {
173 Locator::new(LocatorOverrides {
174 home: Some(PathBuf::from(home)),
175 cwd: Some(PathBuf::from(cwd)),
176 })
177 }
178
179 #[test]
180 fn override_home_drives_every_state_path() {
181 let loc = sandbox("/tmp/box", "/work/project");
182 assert_eq!(loc.profile_dir(), PathBuf::from("/tmp/box/.indusagi"));
183 assert_eq!(
184 loc.settings_path(),
185 PathBuf::from("/tmp/box/.indusagi/settings.json")
186 );
187 assert_eq!(
188 loc.auth_store_path(),
189 PathBuf::from("/tmp/box/.indusagi/auth.json")
190 );
191 assert_eq!(
192 loc.sessions_dir(),
193 PathBuf::from("/tmp/box/.indusagi/sessions")
194 );
195 assert_eq!(loc.logs_dir(), PathBuf::from("/tmp/box/.indusagi/logs"));
196 }
197
198 #[test]
199 fn project_settings_resolve_relative_against_cwd() {
200 let loc = sandbox("/tmp/box", "/work/project");
201 assert_eq!(
203 loc.project_settings_path(Some(Path::new("/abs/repo"))),
204 PathBuf::from("/abs/repo/.indusagi/settings.json")
205 );
206 assert_eq!(
208 loc.project_settings_path(Some(Path::new("sub"))),
209 PathBuf::from("/work/project/sub/.indusagi/settings.json")
210 );
211 assert_eq!(
213 loc.project_settings_path(None),
214 PathBuf::from("/work/project/.indusagi/settings.json")
215 );
216 }
217
218 #[test]
219 fn home_env_var_is_branded() {
220 assert_eq!(home_env_var(), "INDUSAGI_HOME");
221 }
222
223 #[tokio::test]
224 async fn ensure_dir_creates_nested_paths() {
225 let tmp = std::env::temp_dir().join(format!("indusagi-loc-test-{}", crate::ids::new_id()));
226 let loc = Locator::new(LocatorOverrides {
227 home: Some(tmp.clone()),
228 cwd: None,
229 });
230 let sessions = loc.ensure_sessions_dir().await.unwrap();
231 assert!(sessions.is_dir());
232 assert_eq!(sessions, tmp.join(".indusagi/sessions"));
233 let _ = tokio::fs::remove_dir_all(&tmp).await;
235 }
236}