cuenv_core/
paths.rs

1//! Centralized path management for cuenv data directories.
2//!
3//! This module provides platform-appropriate paths following OS conventions:
4//!
5//! | Platform | State Dir | Cache Dir | Runtime Dir |
6//! |----------|-----------|-----------|-------------|
7//! | **macOS** | `~/Library/Application Support/cuenv` | `~/Library/Caches/cuenv` | `$TMPDIR` |
8//! | **Linux** | `~/.local/state/cuenv` (XDG_STATE_HOME) | `~/.cache/cuenv` (XDG_CACHE_HOME) | `/run/user/$UID` (XDG_RUNTIME_DIR) |
9//! | **Windows** | `%APPDATA%\cuenv` | `%LOCALAPPDATA%\cuenv` | `%TEMP%` |
10//!
11//! All functions support environment variable overrides for testing and CI:
12//! - `CUENV_STATE_DIR` - Override state directory
13//! - `CUENV_CACHE_DIR` - Override cache directory
14//! - `CUENV_RUNTIME_DIR` - Override runtime directory
15
16use crate::{Error, Result};
17use std::path::PathBuf;
18
19/// Get the state directory for persistent cuenv data.
20///
21/// State data includes:
22/// - Hook execution state (`state/`)
23/// - Approval records (`approved.json`)
24///
25/// Resolution order:
26/// 1. `CUENV_STATE_DIR` environment variable
27/// 2. Platform state directory + `/cuenv`
28///
29/// # Errors
30///
31/// Returns an error if the home directory cannot be determined.
32pub fn state_dir() -> Result<PathBuf> {
33    if let Ok(dir) = std::env::var("CUENV_STATE_DIR")
34        && !dir.is_empty()
35    {
36        return Ok(PathBuf::from(dir));
37    }
38
39    // Use dirs::state_dir() which returns:
40    // - Linux: XDG_STATE_HOME (~/.local/state)
41    // - macOS: ~/Library/Application Support
42    // - Windows: {FOLDERID_RoamingAppData}
43    //
44    // Note: state_dir() returns None on macOS/Windows, so we fall back to data_dir()
45    let base = dirs::state_dir()
46        .or_else(dirs::data_dir)
47        .ok_or_else(|| Error::configuration("Could not determine state directory"))?;
48
49    Ok(base.join("cuenv"))
50}
51
52/// Get the cache directory for cuenv.
53///
54/// Cache data includes:
55/// - Task execution cache (`tasks/`)
56/// - Task result metadata
57///
58/// Resolution order:
59/// 1. `CUENV_CACHE_DIR` environment variable
60/// 2. Platform cache directory + `/cuenv`
61///
62/// # Errors
63///
64/// Returns an error if the cache directory cannot be determined.
65pub fn cache_dir() -> Result<PathBuf> {
66    if let Ok(dir) = std::env::var("CUENV_CACHE_DIR")
67        && !dir.is_empty()
68    {
69        return Ok(PathBuf::from(dir));
70    }
71
72    let base = dirs::cache_dir()
73        .ok_or_else(|| Error::configuration("Could not determine cache directory"))?;
74
75    Ok(base.join("cuenv"))
76}
77
78/// Get the runtime directory for ephemeral cuenv data.
79///
80/// Runtime data includes:
81/// - Coordinator socket
82/// - PID files
83/// - Lock files
84///
85/// Resolution order:
86/// 1. `CUENV_RUNTIME_DIR` environment variable
87/// 2. Platform runtime directory (or temp directory as fallback)
88///
89/// # Errors
90///
91/// Returns an error if the runtime directory cannot be determined.
92pub fn runtime_dir() -> Result<PathBuf> {
93    if let Ok(dir) = std::env::var("CUENV_RUNTIME_DIR")
94        && !dir.is_empty()
95    {
96        return Ok(PathBuf::from(dir));
97    }
98
99    // runtime_dir() returns:
100    // - Linux: XDG_RUNTIME_DIR (/run/user/$UID)
101    // - macOS/Windows: None (we use temp_dir as fallback)
102    let base = dirs::runtime_dir().unwrap_or_else(std::env::temp_dir);
103
104    Ok(base.join("cuenv"))
105}
106
107/// Get the path to the hook state directory.
108///
109/// This is where hook execution state files are stored.
110pub fn hook_state_dir() -> Result<PathBuf> {
111    Ok(state_dir()?.join("state"))
112}
113
114/// Get the path to the approvals file.
115///
116/// This file tracks which configurations have been approved for hook execution.
117pub fn approvals_file() -> Result<PathBuf> {
118    Ok(state_dir()?.join("approved.json"))
119}
120
121/// Get the path to the task cache directory.
122///
123/// This is where hermetic task execution results are cached.
124pub fn task_cache_dir() -> Result<PathBuf> {
125    Ok(cache_dir()?.join("tasks"))
126}
127
128/// Get the path to the coordinator socket.
129///
130/// The coordinator socket enables multi-UI support (CLI, TUI, Web).
131pub fn coordinator_socket() -> Result<PathBuf> {
132    Ok(runtime_dir()?.join("coordinator.sock"))
133}
134
135/// Get the path to the coordinator PID file.
136pub fn coordinator_pid() -> Result<PathBuf> {
137    Ok(runtime_dir()?.join("coordinator.pid"))
138}
139
140/// Get the path to the coordinator lock file.
141pub fn coordinator_lock() -> Result<PathBuf> {
142    Ok(runtime_dir()?.join("coordinator.lock"))
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn test_state_dir_default() {
151        // Clear override to test default behavior
152        unsafe { std::env::remove_var("CUENV_STATE_DIR") };
153        let dir = state_dir().expect("state_dir should succeed");
154        assert!(dir.ends_with("cuenv"), "Should end with cuenv: {:?}", dir);
155    }
156
157    #[test]
158    fn test_state_dir_override() {
159        let test_dir = "/tmp/cuenv-test-state";
160        unsafe { std::env::set_var("CUENV_STATE_DIR", test_dir) };
161        let dir = state_dir().expect("state_dir should succeed");
162        assert_eq!(dir, PathBuf::from(test_dir));
163        unsafe { std::env::remove_var("CUENV_STATE_DIR") };
164    }
165
166    #[test]
167    fn test_cache_dir_default() {
168        unsafe { std::env::remove_var("CUENV_CACHE_DIR") };
169        let dir = cache_dir().expect("cache_dir should succeed");
170        assert!(dir.ends_with("cuenv"), "Should end with cuenv: {:?}", dir);
171    }
172
173    #[test]
174    fn test_cache_dir_override() {
175        let test_dir = "/tmp/cuenv-test-cache";
176        unsafe { std::env::set_var("CUENV_CACHE_DIR", test_dir) };
177        let dir = cache_dir().expect("cache_dir should succeed");
178        assert_eq!(dir, PathBuf::from(test_dir));
179        unsafe { std::env::remove_var("CUENV_CACHE_DIR") };
180    }
181
182    #[test]
183    fn test_runtime_dir_default() {
184        unsafe { std::env::remove_var("CUENV_RUNTIME_DIR") };
185        let dir = runtime_dir().expect("runtime_dir should succeed");
186        assert!(dir.ends_with("cuenv"), "Should end with cuenv: {:?}", dir);
187    }
188
189    #[test]
190    fn test_runtime_dir_override() {
191        let test_dir = "/tmp/cuenv-test-runtime";
192        unsafe { std::env::set_var("CUENV_RUNTIME_DIR", test_dir) };
193        let dir = runtime_dir().expect("runtime_dir should succeed");
194        assert_eq!(dir, PathBuf::from(test_dir));
195        unsafe { std::env::remove_var("CUENV_RUNTIME_DIR") };
196    }
197
198    #[test]
199    fn test_hook_state_dir() {
200        unsafe { std::env::remove_var("CUENV_STATE_DIR") };
201        let dir = hook_state_dir().expect("hook_state_dir should succeed");
202        assert!(dir.ends_with("state"), "Should end with state: {:?}", dir);
203    }
204
205    #[test]
206    fn test_approvals_file() {
207        unsafe { std::env::remove_var("CUENV_STATE_DIR") };
208        let file = approvals_file().expect("approvals_file should succeed");
209        assert!(
210            file.ends_with("approved.json"),
211            "Should end with approved.json: {:?}",
212            file
213        );
214    }
215
216    #[test]
217    fn test_task_cache_dir() {
218        unsafe { std::env::remove_var("CUENV_CACHE_DIR") };
219        let dir = task_cache_dir().expect("task_cache_dir should succeed");
220        assert!(dir.ends_with("tasks"), "Should end with tasks: {:?}", dir);
221    }
222
223    #[test]
224    fn test_coordinator_paths() {
225        unsafe { std::env::remove_var("CUENV_RUNTIME_DIR") };
226        let socket = coordinator_socket().expect("coordinator_socket should succeed");
227        let pid = coordinator_pid().expect("coordinator_pid should succeed");
228        let lock = coordinator_lock().expect("coordinator_lock should succeed");
229
230        assert!(
231            socket.ends_with("coordinator.sock"),
232            "Socket should end with coordinator.sock: {:?}",
233            socket
234        );
235        assert!(
236            pid.ends_with("coordinator.pid"),
237            "PID should end with coordinator.pid: {:?}",
238            pid
239        );
240        assert!(
241            lock.ends_with("coordinator.lock"),
242            "Lock should end with coordinator.lock: {:?}",
243            lock
244        );
245    }
246}