1use std::path::PathBuf;
17use std::sync::{Arc, RwLock};
18
19#[derive(Clone, Debug)]
25pub struct DaemonPaths {
26 pub base: PathBuf,
29 pub home: PathBuf,
32 pub work_dir: PathBuf,
36 pub extra_skill_dirs: Arc<RwLock<Vec<PathBuf>>>,
43}
44
45impl DaemonPaths {
46 pub fn from_cli(
58 cwd: Option<PathBuf>,
59 home: Option<PathBuf>,
60 extra_skill_dirs: Vec<PathBuf>,
61 ) -> Self {
62 Self::from_cli_with_base(cwd, home, extra_skill_dirs, None)
63 }
64
65 pub fn from_cli_with_base(
67 cwd: Option<PathBuf>,
68 home: Option<PathBuf>,
69 extra_skill_dirs: Vec<PathBuf>,
70 theway_dir: Option<PathBuf>,
71 ) -> Self {
72 let home = home.unwrap_or_else(|| {
73 std::env::var_os("HOME")
74 .map(PathBuf::from)
75 .unwrap_or_else(|| PathBuf::from("."))
76 });
77 let base = theway_dir
78 .or_else(|| std::env::var_os("THEWAY_DIR").map(PathBuf::from))
79 .unwrap_or_else(|| home.join(".theway"));
80 let work_dir = match cwd {
81 Some(dir) => dir,
82 None => std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
83 };
84 let work_dir = work_dir.canonicalize().unwrap_or(work_dir);
85 Self {
86 base,
87 home,
88 work_dir,
89 extra_skill_dirs: Arc::new(RwLock::new(extra_skill_dirs)),
90 }
91 }
92
93 pub fn skills_root(&self) -> PathBuf {
95 self.base.join("skills")
96 }
97
98 pub fn with_work_dir(&self, work_dir: impl Into<PathBuf>) -> Self {
100 let work_dir = work_dir.into();
101 let work_dir = work_dir.canonicalize().unwrap_or(work_dir);
102 Self {
103 base: self.base.clone(),
104 home: self.home.clone(),
105 work_dir,
106 extra_skill_dirs: self.extra_skill_dirs.clone(),
107 }
108 }
109
110 pub fn set_extra_skill_dirs(&self, dirs: Vec<PathBuf>) {
114 *self.extra_skill_dirs.write().unwrap() = dirs;
115 }
116
117 pub fn current_extra_skill_dirs(&self) -> Vec<PathBuf> {
120 self.extra_skill_dirs.read().unwrap().clone()
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
133 use crate::test_env::{ENV_LOCK, EnvGuard};
134
135 fn canonical(path: &std::path::Path) -> PathBuf {
136 path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
137 }
138
139 #[test]
140 fn theway_dir_overrides_home_derived_base() {
141 let _serial = ENV_LOCK.lock().unwrap();
142 let _theway = EnvGuard::set("THEWAY_DIR", "/custom/theway");
143 let _home_env = EnvGuard::set("HOME", "/env-home");
144
145 let paths = DaemonPaths::from_cli(None, Some(PathBuf::from("/flag-home")), Vec::new());
146 assert_eq!(paths.base, PathBuf::from("/custom/theway"));
147 assert_eq!(paths.home, PathBuf::from("/flag-home"));
148 assert_eq!(paths.skills_root(), PathBuf::from("/custom/theway/skills"));
149 }
150
151 #[test]
152 fn explicit_home_overrides_env_home_and_derives_base() {
153 let _serial = ENV_LOCK.lock().unwrap();
154 let _theway = EnvGuard::remove("THEWAY_DIR");
155 let _home_env = EnvGuard::set("HOME", "/env-home");
156
157 let paths = DaemonPaths::from_cli(None, Some(PathBuf::from("/flag-home")), Vec::new());
158 assert_eq!(paths.home, PathBuf::from("/flag-home"));
159 assert_eq!(paths.base, PathBuf::from("/flag-home/.theway"));
160 }
161
162 #[test]
163 fn theway_dir_flag_overrides_env_and_home() {
164 let _serial = ENV_LOCK.lock().unwrap();
165 let _theway = EnvGuard::set("THEWAY_DIR", "/env/theway");
166 let _home_env = EnvGuard::set("HOME", "/env-home");
167
168 let paths = DaemonPaths::from_cli_with_base(
169 None,
170 Some(PathBuf::from("/flag-home")),
171 Vec::new(),
172 Some(PathBuf::from("/custom/theway")),
173 );
174 assert_eq!(paths.base, PathBuf::from("/custom/theway"));
175 assert_eq!(paths.skills_root(), PathBuf::from("/custom/theway/skills"));
176 }
177
178 #[test]
179 fn from_cli_with_base_keeps_env_precedence_without_flag() {
180 let _serial = ENV_LOCK.lock().unwrap();
181 let _theway = EnvGuard::set("THEWAY_DIR", "/env/theway");
182 let _home_env = EnvGuard::set("HOME", "/env-home");
183
184 let paths = DaemonPaths::from_cli_with_base(
185 None,
186 Some(PathBuf::from("/flag-home")),
187 Vec::new(),
188 None,
189 );
190 assert_eq!(paths.base, PathBuf::from("/env/theway"));
191 }
192
193 #[test]
194 fn from_cli_with_base_defaults_to_home_theway() {
195 let _serial = ENV_LOCK.lock().unwrap();
196 let _theway = EnvGuard::remove("THEWAY_DIR");
197 let _home_env = EnvGuard::set("HOME", "/env-home");
198
199 let paths = DaemonPaths::from_cli_with_base(
200 None,
201 Some(PathBuf::from("/flag-home")),
202 Vec::new(),
203 None,
204 );
205 assert_eq!(paths.base, PathBuf::from("/flag-home/.theway"));
206 }
207
208 #[test]
209 fn env_home_derives_base_when_no_flag() {
210 let _serial = ENV_LOCK.lock().unwrap();
211 let _theway = EnvGuard::remove("THEWAY_DIR");
212 let _home_env = EnvGuard::set("HOME", "/env-home");
213
214 let paths = DaemonPaths::from_cli(None, None, Vec::new());
215 assert_eq!(paths.home, PathBuf::from("/env-home"));
216 assert_eq!(paths.base, PathBuf::from("/env-home/.theway"));
217 }
218
219 #[test]
220 fn work_dir_falls_back_to_process_cwd() {
221 let _serial = ENV_LOCK.lock().unwrap();
222 let _theway = EnvGuard::remove("THEWAY_DIR");
223
224 let paths = DaemonPaths::from_cli(None, Some(PathBuf::from("/h")), Vec::new());
225 let expected = std::env::current_dir().unwrap();
226 assert_eq!(paths.work_dir, canonical(&expected));
227 }
228
229 #[test]
230 fn explicit_work_dir_wins_and_survives_failed_canonicalize() {
231 let _serial = ENV_LOCK.lock().unwrap();
232 let _theway = EnvGuard::remove("THEWAY_DIR");
233
234 let temp = tempfile::tempdir().unwrap();
236 let paths = DaemonPaths::from_cli(Some(temp.path().to_path_buf()), None, Vec::new());
237 assert_eq!(paths.work_dir, canonical(temp.path()));
238
239 let missing = PathBuf::from("/nonexistent-theway-work-dir-66");
242 let paths = DaemonPaths::from_cli(Some(missing.clone()), None, Vec::new());
243 assert_eq!(paths.work_dir, missing);
244 }
245
246 #[test]
247 fn extra_skill_dirs_are_carried_through() {
248 let _serial = ENV_LOCK.lock().unwrap();
249 let _theway = EnvGuard::remove("THEWAY_DIR");
250
251 let extras = vec![PathBuf::from("/a/skills"), PathBuf::from("/b/skills")];
252 let paths = DaemonPaths::from_cli(None, Some(PathBuf::from("/h")), extras.clone());
253 assert_eq!(paths.current_extra_skill_dirs(), extras);
254 }
255
256 #[test]
257 fn extra_skill_dirs_update_dynamically() {
258 let _serial = ENV_LOCK.lock().unwrap();
259 let _theway = EnvGuard::remove("THEWAY_DIR");
260
261 let paths = DaemonPaths::from_cli(
262 None,
263 Some(PathBuf::from("/h")),
264 vec![PathBuf::from("/a/skills")],
265 );
266 assert_eq!(
267 paths.current_extra_skill_dirs(),
268 vec![PathBuf::from("/a/skills")]
269 );
270
271 paths.set_extra_skill_dirs(vec![PathBuf::from("/x/skills"), PathBuf::from("/y/skills")]);
274 assert_eq!(
275 paths.current_extra_skill_dirs(),
276 vec![PathBuf::from("/x/skills"), PathBuf::from("/y/skills")]
277 );
278
279 paths.set_extra_skill_dirs(Vec::new());
281 assert!(paths.current_extra_skill_dirs().is_empty());
282 }
283
284 #[test]
285 fn with_work_dir_preserves_shared_base_home_and_extra_skill_dirs() {
286 let _serial = ENV_LOCK.lock().unwrap();
287 let _theway = EnvGuard::remove("THEWAY_DIR");
288 let _home_env = EnvGuard::set("HOME", "/env-home");
289
290 let paths = DaemonPaths::from_cli(
291 None,
292 Some(PathBuf::from("/flag-home")),
293 vec![PathBuf::from("/shared/skills")],
294 );
295 let other = tempfile::tempdir().unwrap();
296 let derived = paths.with_work_dir(other.path());
297
298 assert_eq!(derived.base, paths.base);
299 assert_eq!(derived.home, paths.home);
300 assert_eq!(derived.work_dir, canonical(other.path()));
301 assert!(Arc::ptr_eq(
302 &derived.extra_skill_dirs,
303 &paths.extra_skill_dirs
304 ));
305
306 paths.set_extra_skill_dirs(vec![PathBuf::from("/updated/skills")]);
307 assert_eq!(
308 derived.current_extra_skill_dirs(),
309 vec![PathBuf::from("/updated/skills")]
310 );
311 }
312
313 #[test]
314 fn extra_skill_dirs_shared_across_clones() {
315 let _serial = ENV_LOCK.lock().unwrap();
316 let _theway = EnvGuard::remove("THEWAY_DIR");
317
318 let paths = DaemonPaths::from_cli(None, Some(PathBuf::from("/h")), Vec::new());
319 let cloned = paths.clone();
320
321 paths.set_extra_skill_dirs(vec![PathBuf::from("/shared/skills")]);
325 assert_eq!(
326 cloned.current_extra_skill_dirs(),
327 vec![PathBuf::from("/shared/skills")]
328 );
329 cloned.set_extra_skill_dirs(Vec::new());
330 assert!(paths.current_extra_skill_dirs().is_empty());
331 }
332}