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