1use std::path::{Path, PathBuf};
2
3pub const HARN_STATE_DIR_ENV: &str = "HARN_STATE_DIR";
4pub const HARN_RUN_DIR_ENV: &str = "HARN_RUN_DIR";
5pub const HARN_WORKTREE_DIR_ENV: &str = "HARN_WORKTREE_DIR";
6
7#[cfg(test)]
8pub(crate) fn test_env_lock() -> &'static std::sync::Mutex<()> {
9 use std::sync::{Mutex, OnceLock};
10 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
11 LOCK.get_or_init(|| Mutex::new(()))
12}
13
14fn resolve_root_value(base_dir: &Path, env_value: Option<&str>, default_relative: &str) -> PathBuf {
15 match env_value {
16 Some(value) if !value.trim().is_empty() => {
17 let candidate = PathBuf::from(value);
18 if candidate.is_absolute() {
19 candidate
20 } else {
21 base_dir.join(candidate)
22 }
23 }
24 _ => base_dir.join(default_relative),
25 }
26}
27
28fn resolve_root(base_dir: &Path, env_key: &str, default_relative: &str) -> PathBuf {
29 let env_value = std::env::var(env_key).ok();
30 resolve_root_value(base_dir, env_value.as_deref(), default_relative)
31}
32
33pub fn state_root(base_dir: &Path) -> PathBuf {
34 resolve_root(base_dir, HARN_STATE_DIR_ENV, ".harn")
35}
36
37pub fn run_root(base_dir: &Path) -> PathBuf {
38 resolve_root(base_dir, HARN_RUN_DIR_ENV, ".harn-runs")
39}
40
41fn worktree_root_value(
42 base_dir: &Path,
43 state_env_value: Option<&str>,
44 worktree_env_value: Option<&str>,
45) -> PathBuf {
46 match worktree_env_value {
47 Some(value) if !value.trim().is_empty() => {
48 let candidate = PathBuf::from(value);
49 if candidate.is_absolute() {
50 candidate
51 } else {
52 base_dir.join(candidate)
53 }
54 }
55 _ => resolve_root_value(base_dir, state_env_value, ".harn").join("worktrees"),
56 }
57}
58
59pub fn worktree_root(base_dir: &Path) -> PathBuf {
60 let state_env_value = std::env::var(HARN_STATE_DIR_ENV).ok();
61 let worktree_env_value = std::env::var(HARN_WORKTREE_DIR_ENV).ok();
62 worktree_root_value(
63 base_dir,
64 state_env_value.as_deref(),
65 worktree_env_value.as_deref(),
66 )
67}
68
69pub fn store_path(base_dir: &Path) -> PathBuf {
70 state_root(base_dir).join("store.json")
71}
72
73pub fn checkpoint_dir(base_dir: &Path) -> PathBuf {
74 state_root(base_dir).join("checkpoints")
75}
76
77pub fn metadata_dir(base_dir: &Path) -> PathBuf {
78 state_root(base_dir).join("metadata")
79}
80
81pub fn event_log_dir(base_dir: &Path) -> PathBuf {
82 state_root(base_dir).join("events")
83}
84
85pub fn event_log_sqlite_path(base_dir: &Path) -> PathBuf {
86 state_root(base_dir).join("events.sqlite")
87}
88
89pub fn workflow_dir(base_dir: &Path) -> PathBuf {
90 state_root(base_dir).join("workflows")
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn defaults_resolve_under_base_dir() {
99 let base = Path::new("/tmp/harn-runtime-paths");
100 assert_eq!(resolve_root_value(base, None, ".harn"), base.join(".harn"));
101 assert_eq!(
102 resolve_root_value(base, None, ".harn-runs"),
103 base.join(".harn-runs")
104 );
105 assert_eq!(
106 worktree_root_value(base, None, None),
107 base.join(".harn").join("worktrees")
108 );
109 assert_eq!(
110 resolve_root_value(base, None, ".harn").join("events"),
111 base.join(".harn").join("events")
112 );
113 assert_eq!(
114 resolve_root_value(base, None, ".harn").join("workflows"),
115 base.join(".harn").join("workflows")
116 );
117 assert_eq!(
118 resolve_root_value(base, None, ".harn").join("events.sqlite"),
119 base.join(".harn").join("events.sqlite")
120 );
121 }
122}