thoughts_tool/utils/
paths.rs

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