use std::path::PathBuf;
use std::sync::{Arc, RwLock};
#[derive(Clone, Debug)]
pub struct DaemonPaths {
pub base: PathBuf,
pub home: PathBuf,
pub work_dir: PathBuf,
pub extra_skill_dirs: Arc<RwLock<Vec<PathBuf>>>,
}
impl DaemonPaths {
pub fn from_cli(
cwd: Option<PathBuf>,
home: Option<PathBuf>,
extra_skill_dirs: Vec<PathBuf>,
) -> Self {
Self::from_cli_with_base(cwd, home, extra_skill_dirs, None)
}
pub fn from_cli_with_base(
cwd: Option<PathBuf>,
home: Option<PathBuf>,
extra_skill_dirs: Vec<PathBuf>,
theway_dir: Option<PathBuf>,
) -> Self {
let home = home.unwrap_or_else(|| {
std::env::var_os("HOME")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."))
});
let base = theway_dir
.or_else(|| std::env::var_os("THEWAY_DIR").map(PathBuf::from))
.unwrap_or_else(|| home.join(".theway"));
let work_dir = match cwd {
Some(dir) => dir,
None => std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
};
let work_dir = work_dir.canonicalize().unwrap_or(work_dir);
Self {
base,
home,
work_dir,
extra_skill_dirs: Arc::new(RwLock::new(extra_skill_dirs)),
}
}
pub fn skills_root(&self) -> PathBuf {
self.base.join("skills")
}
pub fn with_work_dir(&self, work_dir: impl Into<PathBuf>) -> Self {
let work_dir = work_dir.into();
let work_dir = work_dir.canonicalize().unwrap_or(work_dir);
Self {
base: self.base.clone(),
home: self.home.clone(),
work_dir,
extra_skill_dirs: self.extra_skill_dirs.clone(),
}
}
pub fn set_extra_skill_dirs(&self, dirs: Vec<PathBuf>) {
*self.extra_skill_dirs.write().unwrap() = dirs;
}
pub fn current_extra_skill_dirs(&self) -> Vec<PathBuf> {
self.extra_skill_dirs.read().unwrap().clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_env::{ENV_LOCK, EnvGuard};
fn canonical(path: &std::path::Path) -> PathBuf {
path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
}
#[test]
fn theway_dir_overrides_home_derived_base() {
let _serial = ENV_LOCK.lock().unwrap();
let _theway = EnvGuard::set("THEWAY_DIR", "/custom/theway");
let _home_env = EnvGuard::set("HOME", "/env-home");
let paths = DaemonPaths::from_cli(None, Some(PathBuf::from("/flag-home")), Vec::new());
assert_eq!(paths.base, PathBuf::from("/custom/theway"));
assert_eq!(paths.home, PathBuf::from("/flag-home"));
assert_eq!(paths.skills_root(), PathBuf::from("/custom/theway/skills"));
}
#[test]
fn explicit_home_overrides_env_home_and_derives_base() {
let _serial = ENV_LOCK.lock().unwrap();
let _theway = EnvGuard::remove("THEWAY_DIR");
let _home_env = EnvGuard::set("HOME", "/env-home");
let paths = DaemonPaths::from_cli(None, Some(PathBuf::from("/flag-home")), Vec::new());
assert_eq!(paths.home, PathBuf::from("/flag-home"));
assert_eq!(paths.base, PathBuf::from("/flag-home/.theway"));
}
#[test]
fn theway_dir_flag_overrides_env_and_home() {
let _serial = ENV_LOCK.lock().unwrap();
let _theway = EnvGuard::set("THEWAY_DIR", "/env/theway");
let _home_env = EnvGuard::set("HOME", "/env-home");
let paths = DaemonPaths::from_cli_with_base(
None,
Some(PathBuf::from("/flag-home")),
Vec::new(),
Some(PathBuf::from("/custom/theway")),
);
assert_eq!(paths.base, PathBuf::from("/custom/theway"));
assert_eq!(paths.skills_root(), PathBuf::from("/custom/theway/skills"));
}
#[test]
fn from_cli_with_base_keeps_env_precedence_without_flag() {
let _serial = ENV_LOCK.lock().unwrap();
let _theway = EnvGuard::set("THEWAY_DIR", "/env/theway");
let _home_env = EnvGuard::set("HOME", "/env-home");
let paths = DaemonPaths::from_cli_with_base(
None,
Some(PathBuf::from("/flag-home")),
Vec::new(),
None,
);
assert_eq!(paths.base, PathBuf::from("/env/theway"));
}
#[test]
fn from_cli_with_base_defaults_to_home_theway() {
let _serial = ENV_LOCK.lock().unwrap();
let _theway = EnvGuard::remove("THEWAY_DIR");
let _home_env = EnvGuard::set("HOME", "/env-home");
let paths = DaemonPaths::from_cli_with_base(
None,
Some(PathBuf::from("/flag-home")),
Vec::new(),
None,
);
assert_eq!(paths.base, PathBuf::from("/flag-home/.theway"));
}
#[test]
fn env_home_derives_base_when_no_flag() {
let _serial = ENV_LOCK.lock().unwrap();
let _theway = EnvGuard::remove("THEWAY_DIR");
let _home_env = EnvGuard::set("HOME", "/env-home");
let paths = DaemonPaths::from_cli(None, None, Vec::new());
assert_eq!(paths.home, PathBuf::from("/env-home"));
assert_eq!(paths.base, PathBuf::from("/env-home/.theway"));
}
#[test]
fn work_dir_falls_back_to_process_cwd() {
let _serial = ENV_LOCK.lock().unwrap();
let _theway = EnvGuard::remove("THEWAY_DIR");
let paths = DaemonPaths::from_cli(None, Some(PathBuf::from("/h")), Vec::new());
let expected = std::env::current_dir().unwrap();
assert_eq!(paths.work_dir, canonical(&expected));
}
#[test]
fn explicit_work_dir_wins_and_survives_failed_canonicalize() {
let _serial = ENV_LOCK.lock().unwrap();
let _theway = EnvGuard::remove("THEWAY_DIR");
let temp = tempfile::tempdir().unwrap();
let paths = DaemonPaths::from_cli(Some(temp.path().to_path_buf()), None, Vec::new());
assert_eq!(paths.work_dir, canonical(temp.path()));
let missing = PathBuf::from("/nonexistent-theway-work-dir-66");
let paths = DaemonPaths::from_cli(Some(missing.clone()), None, Vec::new());
assert_eq!(paths.work_dir, missing);
}
#[test]
fn extra_skill_dirs_are_carried_through() {
let _serial = ENV_LOCK.lock().unwrap();
let _theway = EnvGuard::remove("THEWAY_DIR");
let extras = vec![PathBuf::from("/a/skills"), PathBuf::from("/b/skills")];
let paths = DaemonPaths::from_cli(None, Some(PathBuf::from("/h")), extras.clone());
assert_eq!(paths.current_extra_skill_dirs(), extras);
}
#[test]
fn extra_skill_dirs_update_dynamically() {
let _serial = ENV_LOCK.lock().unwrap();
let _theway = EnvGuard::remove("THEWAY_DIR");
let paths = DaemonPaths::from_cli(
None,
Some(PathBuf::from("/h")),
vec![PathBuf::from("/a/skills")],
);
assert_eq!(
paths.current_extra_skill_dirs(),
vec![PathBuf::from("/a/skills")]
);
paths.set_extra_skill_dirs(vec![PathBuf::from("/x/skills"), PathBuf::from("/y/skills")]);
assert_eq!(
paths.current_extra_skill_dirs(),
vec![PathBuf::from("/x/skills"), PathBuf::from("/y/skills")]
);
paths.set_extra_skill_dirs(Vec::new());
assert!(paths.current_extra_skill_dirs().is_empty());
}
#[test]
fn with_work_dir_preserves_shared_base_home_and_extra_skill_dirs() {
let _serial = ENV_LOCK.lock().unwrap();
let _theway = EnvGuard::remove("THEWAY_DIR");
let _home_env = EnvGuard::set("HOME", "/env-home");
let paths = DaemonPaths::from_cli(
None,
Some(PathBuf::from("/flag-home")),
vec![PathBuf::from("/shared/skills")],
);
let other = tempfile::tempdir().unwrap();
let derived = paths.with_work_dir(other.path());
assert_eq!(derived.base, paths.base);
assert_eq!(derived.home, paths.home);
assert_eq!(derived.work_dir, canonical(other.path()));
assert!(Arc::ptr_eq(
&derived.extra_skill_dirs,
&paths.extra_skill_dirs
));
paths.set_extra_skill_dirs(vec![PathBuf::from("/updated/skills")]);
assert_eq!(
derived.current_extra_skill_dirs(),
vec![PathBuf::from("/updated/skills")]
);
}
#[test]
fn extra_skill_dirs_shared_across_clones() {
let _serial = ENV_LOCK.lock().unwrap();
let _theway = EnvGuard::remove("THEWAY_DIR");
let paths = DaemonPaths::from_cli(None, Some(PathBuf::from("/h")), Vec::new());
let cloned = paths.clone();
paths.set_extra_skill_dirs(vec![PathBuf::from("/shared/skills")]);
assert_eq!(
cloned.current_extra_skill_dirs(),
vec![PathBuf::from("/shared/skills")]
);
cloned.set_extra_skill_dirs(Vec::new());
assert!(paths.current_extra_skill_dirs().is_empty());
}
}