Skip to main content

thoughts_tool/utils/
paths.rs

1use anyhow::Result;
2use dirs;
3use std::path::Path;
4use std::path::PathBuf;
5
6/// Expand tilde (~) in paths to home directory
7pub fn expand_path(path: &Path) -> Result<PathBuf> {
8    let path_str = path.to_string_lossy();
9
10    if let Some(stripped) = path_str.strip_prefix("~/") {
11        let home = dirs::home_dir()
12            .ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
13        Ok(home.join(stripped))
14    } else if path_str == "~" {
15        dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))
16    } else {
17        Ok(path.to_path_buf())
18    }
19}
20
21/// Ensure a directory exists, creating it if necessary
22pub fn ensure_dir(path: &Path) -> Result<()> {
23    if !path.exists() {
24        std::fs::create_dir_all(path)?;
25    }
26    Ok(())
27}
28
29/// Sanitize a directory name for use in filesystem
30pub fn sanitize_dir_name(name: &str) -> String {
31    name.chars()
32        .map(|c| match c {
33            '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
34            _ => c,
35        })
36        .collect()
37}
38
39// Add after line 50 (after sanitize_dir_name function)
40
41/// Get the repository configuration file path
42pub fn get_repo_config_path(repo_root: &Path) -> PathBuf {
43    repo_root.join(".thoughts").join("config.json")
44}
45
46/// Get external metadata directory for personal metadata about other repos
47#[cfg(target_os = "macos")]
48pub fn get_external_metadata_dir() -> Result<PathBuf> {
49    let home =
50        dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
51    Ok(home.join(".thoughts").join("data").join("external"))
52}
53
54/// Get local metadata file path for a repository
55#[allow(dead_code)]
56// TODO(2): Implement local metadata caching
57pub fn get_local_metadata_path(repo_root: &Path) -> PathBuf {
58    repo_root.join(".thoughts").join("data").join("local.json")
59}
60
61/// Get rules file path for a repository
62#[allow(dead_code)]
63// TODO(2): Implement repository-specific rules system
64pub fn get_repo_rules_path(repo_root: &Path) -> PathBuf {
65    repo_root.join(".thoughts").join("rules.json")
66}
67
68/// Get the XDG config home directory.
69///
70/// Returns `$XDG_CONFIG_HOME` if set, otherwise `~/.config`.
71fn xdg_config_home() -> Result<PathBuf> {
72    if let Some(dir) = std::env::var_os("XDG_CONFIG_HOME") {
73        return Ok(PathBuf::from(dir));
74    }
75    let home =
76        dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
77    Ok(home.join(".config"))
78}
79
80/// Get the repository mapping file path.
81///
82/// Returns the location at `~/.config/agentic/repos.json`.
83pub fn get_repo_mapping_path() -> Result<PathBuf> {
84    Ok(xdg_config_home()?.join("agentic").join("repos.json"))
85}
86
87/// Get the legacy repository mapping file path.
88///
89/// Returns the old location at `~/.thoughts/repos.json` for migration purposes.
90pub fn get_legacy_repo_mapping_path() -> Result<PathBuf> {
91    let home =
92        dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
93    Ok(home.join(".thoughts").join("repos.json"))
94}
95
96/// Get the personal config path (for deprecation warnings)
97pub fn get_personal_config_path() -> Result<PathBuf> {
98    let home =
99        dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
100    Ok(home.join(".thoughts").join("config.json"))
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use serial_test::serial;
107
108    #[test]
109    #[serial]
110    fn test_expand_path() {
111        // Test tilde expansion
112        let home = dirs::home_dir().unwrap();
113        assert_eq!(expand_path(Path::new("~/test")).unwrap(), home.join("test"));
114        assert_eq!(expand_path(Path::new("~")).unwrap(), home);
115
116        // Test absolute path
117        assert_eq!(
118            expand_path(Path::new("/tmp/test")).unwrap(),
119            PathBuf::from("/tmp/test")
120        );
121
122        // Test relative path
123        assert_eq!(
124            expand_path(Path::new("test")).unwrap(),
125            PathBuf::from("test")
126        );
127    }
128
129    #[test]
130    fn test_sanitize_dir_name() {
131        assert_eq!(sanitize_dir_name("normal-name_123"), "normal-name_123");
132        assert_eq!(
133            sanitize_dir_name("bad/name:with*chars?"),
134            "bad_name_with_chars_"
135        );
136    }
137}