Skip to main content

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 local environment root for storing workspace registry and managed workspaces.
32    /// Can be overridden with STEER_ENV_ROOT.
33    pub fn local_environment_root() -> PathBuf {
34        if let Ok(root) = std::env::var("STEER_ENV_ROOT") {
35            return PathBuf::from(root);
36        }
37        if let Some(data_dir) = Self::user_data_dir() {
38            return data_dir.join("envs").join("local");
39        }
40        if let Some(home_dir) = dirs::home_dir() {
41            return home_dir.join(".steer").join("envs").join("local");
42        }
43        PathBuf::from(".steer/envs/local")
44    }
45
46    /// Return the user-level catalog path (platform-specific)
47    pub fn user_catalog() -> Option<PathBuf> {
48        Self::user_config_dir().map(|d| d.join("catalog.toml"))
49    }
50
51    /// Standard discovery order for catalog files
52    /// Project catalog first, then user catalog
53    pub fn discover_catalogs() -> Vec<PathBuf> {
54        let mut paths = Vec::new();
55        paths.push(Self::project_catalog());
56        if let Some(user_cat) = Self::user_catalog() {
57            paths.push(user_cat);
58        }
59        paths
60    }
61
62    /// Standard discovery order for session config files.
63    pub fn discover_session_configs() -> Vec<PathBuf> {
64        let mut paths = Vec::new();
65        // ./.steer/session.toml, then user-level session.toml
66        let project = Self::project_dir().join("session.toml");
67        if project.exists() {
68            paths.push(project);
69        }
70        if let Some(user_cfg) = Self::user_config_dir() {
71            let user = user_cfg.join("session.toml");
72            if user.exists() {
73                paths.push(user);
74            }
75        }
76        paths
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn project_paths_are_static() {
86        assert_eq!(AppPaths::project_dir(), PathBuf::from(".steer"));
87        assert_eq!(
88            AppPaths::project_catalog(),
89            PathBuf::from(".steer/catalog.toml")
90        );
91    }
92
93    #[test]
94    fn discover_catalogs_order_is_deterministic() {
95        let catalogs = AppPaths::discover_catalogs();
96        // Expect exactly 1 or 2 entries: project first, optional user second
97        assert!(catalogs.len() == 1 || catalogs.len() == 2);
98        assert_eq!(catalogs[0], PathBuf::from(".steer/catalog.toml"));
99        if catalogs.len() == 2 {
100            let expected_user =
101                AppPaths::user_catalog().expect("user catalog path should exist when returned");
102            assert_eq!(catalogs[1], expected_user);
103        }
104    }
105
106    #[test]
107    fn discover_session_configs_order_is_deterministic() {
108        let configs = AppPaths::discover_session_configs();
109        // Expect 0, 1, or 2 entries and preserve order
110        assert!(configs.len() <= 2);
111        if configs.len() == 2 {
112            // When both exist, project comes before user
113            assert_eq!(configs[0], PathBuf::from(".steer/session.toml"));
114            let expected_user = AppPaths::user_config_dir().unwrap().join("session.toml");
115            assert_eq!(configs[1], expected_user);
116        } else if configs.len() == 1 {
117            // Single entry may be either project or user-level file
118            let single = &configs[0];
119            let is_project = *single == PathBuf::from(".steer/session.toml");
120            let is_user =
121                AppPaths::user_config_dir().is_some_and(|d| single == &d.join("session.toml"));
122            assert!(is_project || is_user);
123        }
124    }
125}