steer_core/utils/
paths.rs

1use std::path::PathBuf;
2
3/// Standardized application directories for Steer.
4///
5/// - Project-level: ./.steer
6/// - User-level config: uses OS-specific dirs
7/// - User-level data: uses OS-specific dirs
8pub struct AppPaths;
9
10impl AppPaths {
11    /// Return the project-level .steer directory (relative to current working dir)
12    pub fn project_dir() -> PathBuf {
13        PathBuf::from(".steer")
14    }
15
16    /// Return the project-level catalog path: ./.steer/catalog.toml
17    pub fn project_catalog() -> PathBuf {
18        Self::project_dir().join("catalog.toml")
19    }
20
21    /// Return the user-level config directory (platform-specific)
22    pub fn user_config_dir() -> Option<PathBuf> {
23        directories::ProjectDirs::from("", "", "steer").map(|d| d.config_dir().to_path_buf())
24    }
25
26    /// Return the user-level data directory (platform-specific)
27    pub fn user_data_dir() -> Option<PathBuf> {
28        directories::ProjectDirs::from("", "", "steer").map(|d| d.data_dir().to_path_buf())
29    }
30
31    /// Return the user-level catalog path (platform-specific)
32    pub fn user_catalog() -> Option<PathBuf> {
33        Self::user_config_dir().map(|d| d.join("catalog.toml"))
34    }
35
36    /// Standard discovery order for catalog files
37    /// Project catalog first, then user catalog
38    pub fn discover_catalogs() -> Vec<PathBuf> {
39        let mut paths = Vec::new();
40        paths.push(Self::project_catalog());
41        if let Some(user_cat) = Self::user_catalog() {
42            paths.push(user_cat);
43        }
44        paths
45    }
46
47    /// Standard discovery order for session config files.
48    pub fn discover_session_configs() -> Vec<PathBuf> {
49        let mut paths = Vec::new();
50        // ./.steer/session.toml, then user-level session.toml
51        let project = Self::project_dir().join("session.toml");
52        if project.exists() {
53            paths.push(project);
54        }
55        if let Some(user_cfg) = Self::user_config_dir() {
56            let user = user_cfg.join("session.toml");
57            if user.exists() {
58                paths.push(user);
59            }
60        }
61        paths
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn project_paths_are_static() {
71        assert_eq!(AppPaths::project_dir(), PathBuf::from(".steer"));
72        assert_eq!(
73            AppPaths::project_catalog(),
74            PathBuf::from(".steer/catalog.toml")
75        );
76    }
77
78    #[test]
79    fn discover_catalogs_order_is_deterministic() {
80        let catalogs = AppPaths::discover_catalogs();
81        // Expect exactly 1 or 2 entries: project first, optional user second
82        assert!(catalogs.len() == 1 || catalogs.len() == 2);
83        assert_eq!(catalogs[0], PathBuf::from(".steer/catalog.toml"));
84        if catalogs.len() == 2 {
85            let expected_user =
86                AppPaths::user_catalog().expect("user catalog path should exist when returned");
87            assert_eq!(catalogs[1], expected_user);
88        }
89    }
90
91    #[test]
92    fn discover_session_configs_order_is_deterministic() {
93        let configs = AppPaths::discover_session_configs();
94        // Expect 0, 1, or 2 entries and preserve order
95        assert!(configs.len() <= 2);
96        if configs.len() == 2 {
97            // When both exist, project comes before user
98            assert_eq!(configs[0], PathBuf::from(".steer/session.toml"));
99            let expected_user = AppPaths::user_config_dir().unwrap().join("session.toml");
100            assert_eq!(configs[1], expected_user);
101        } else if configs.len() == 1 {
102            // Single entry may be either project or user-level file
103            let single = &configs[0];
104            let is_project = *single == PathBuf::from(".steer/session.toml");
105            let is_user = AppPaths::user_config_dir()
106                .map(|d| single == &d.join("session.toml"))
107                .unwrap_or(false);
108            assert!(is_project || is_user);
109        }
110    }
111}