Skip to main content

commit_wizard/engine/constants/
paths.rs

1use crate::engine::{
2    constants::{
3        CACHE_DIR_NAME, CONFIG_DIR_NAME, CONFIG_FILE_NAME, PROJECT_CONFIG_FILE_NAME,
4        PROJECT_CONFIG_FILE_NAME_HIDDEN, RULES_FILE_NAME, STATE_DIR_NAME,
5    },
6    error::{ErrorCode, Result},
7};
8use std::{
9    env,
10    path::{Path, PathBuf},
11};
12
13pub fn resolve_repo_config_path(cwd: &Path) -> PathBuf {
14    let hidden = cwd.join(PROJECT_CONFIG_FILE_NAME_HIDDEN);
15    let visible = cwd.join(PROJECT_CONFIG_FILE_NAME);
16
17    if hidden.exists() {
18        hidden
19    } else if visible.exists() {
20        visible
21    } else {
22        hidden
23    }
24}
25
26pub fn resolve_new_repo_config_path(cwd: &Path, hidden: bool) -> PathBuf {
27    if hidden {
28        cwd.join(PROJECT_CONFIG_FILE_NAME_HIDDEN) // .cwizard.toml
29    } else {
30        cwd.join(PROJECT_CONFIG_FILE_NAME) // cwizard.toml
31    }
32}
33
34pub fn resolve_global_config_path() -> Result<PathBuf> {
35    Ok(app_config_dir()?.join(CONFIG_FILE_NAME))
36}
37
38pub fn resolve_global_rules_path() -> Result<PathBuf> {
39    Ok(app_config_dir()?.join(RULES_FILE_NAME))
40}
41
42pub fn app_config_dir() -> Result<PathBuf> {
43    #[cfg(target_os = "windows")]
44    {
45        env::var_os("APPDATA")
46            .map(PathBuf::from)
47            .map(|p| p.join(CONFIG_DIR_NAME))
48            .ok_or_else(|| {
49                ErrorCode::ConfigInvalid
50                    .error()
51                    .with_context("reason", "APPDATA is not set")
52            })
53    }
54
55    #[cfg(not(target_os = "windows"))]
56    {
57        if let Some(xdg_config_home) = env::var_os("XDG_CONFIG_HOME") {
58            return Ok(PathBuf::from(xdg_config_home).join(CONFIG_DIR_NAME));
59        }
60
61        env::var_os("HOME")
62            .map(PathBuf::from)
63            .map(|p| p.join(".config").join(CONFIG_DIR_NAME))
64            .ok_or_else(|| {
65                ErrorCode::ConfigInvalid
66                    .error()
67                    .with_context("reason", "neither XDG_CONFIG_HOME nor HOME is set")
68            })
69    }
70}
71
72pub fn app_state_dir() -> Result<PathBuf> {
73    #[cfg(target_os = "windows")]
74    {
75        env::var_os("LOCALAPPDATA")
76            .map(PathBuf::from)
77            .map(|p| p.join(STATE_DIR_NAME))
78            .ok_or_else(|| {
79                ErrorCode::ConfigInvalid
80                    .error()
81                    .with_context("reason", "LOCALAPPDATA is not set")
82            })
83    }
84
85    #[cfg(not(target_os = "windows"))]
86    {
87        if let Some(xdg_state_home) = env::var_os("XDG_STATE_HOME") {
88            return Ok(PathBuf::from(xdg_state_home).join(STATE_DIR_NAME));
89        }
90
91        env::var_os("HOME")
92            .map(PathBuf::from)
93            .map(|p| p.join(".local").join("state").join(STATE_DIR_NAME))
94            .ok_or_else(|| {
95                ErrorCode::ConfigInvalid
96                    .error()
97                    .with_context("reason", "neither XDG_STATE_HOME nor HOME is set")
98            })
99    }
100}
101
102pub fn app_cache_dir() -> Result<PathBuf> {
103    #[cfg(target_os = "windows")]
104    {
105        env::var_os("LOCALAPPDATA")
106            .map(PathBuf::from)
107            .map(|p| p.join(CACHE_DIR_NAME))
108            .ok_or_else(|| {
109                ErrorCode::ConfigInvalid
110                    .error()
111                    .with_context("reason", "LOCALAPPDATA is not set")
112            })
113    }
114
115    #[cfg(not(target_os = "windows"))]
116    {
117        if let Some(xdg_cache_home) = env::var_os("XDG_CACHE_HOME") {
118            return Ok(PathBuf::from(xdg_cache_home).join(CACHE_DIR_NAME));
119        }
120
121        env::var_os("HOME")
122            .map(PathBuf::from)
123            .map(|p| p.join(".cache").join(CACHE_DIR_NAME))
124            .ok_or_else(|| {
125                ErrorCode::ConfigInvalid
126                    .error()
127                    .with_context("reason", "neither XDG_CACHE_HOME nor HOME is set")
128            })
129    }
130}
131
132pub fn resolve_project_config_path(
133    cwd: &Path,
134    repo_root: Option<&Path>,
135    in_git_repo: bool,
136    explicit_config_path: Option<&Path>,
137) -> Option<PathBuf> {
138    if let Some(path) = explicit_config_path {
139        return Some(path.to_path_buf());
140    }
141
142    // Check cwd first — a config here takes precedence over the repo root
143    if let Some(path) = find_project_config_in_dir(cwd) {
144        return Some(path);
145    }
146
147    // Fall back to repo root if we're inside a git repo and cwd isn't the root
148    if in_git_repo
149        && let Some(root) = repo_root
150        && root != cwd
151        && let Some(path) = find_project_config_in_dir(root)
152    {
153        return Some(path);
154    }
155
156    None
157}
158
159pub fn resolve_new_project_config_path(
160    cwd: &Path,
161    repo_root: Option<&Path>,
162    in_git_repo: bool,
163    explicit_config_path: Option<&Path>,
164    hidden: bool,
165) -> PathBuf {
166    if let Some(path) = explicit_config_path {
167        return path.to_path_buf();
168    }
169
170    let base_dir = if in_git_repo {
171        repo_root.unwrap_or(cwd)
172    } else {
173        cwd
174    };
175
176    base_dir.join(if hidden {
177        PROJECT_CONFIG_FILE_NAME_HIDDEN
178    } else {
179        PROJECT_CONFIG_FILE_NAME
180    })
181}
182
183fn find_project_config_in_dir(dir: &Path) -> Option<PathBuf> {
184    let hidden = dir.join(PROJECT_CONFIG_FILE_NAME_HIDDEN);
185    if hidden.exists() {
186        return Some(hidden);
187    }
188
189    let visible = dir.join(PROJECT_CONFIG_FILE_NAME);
190    if visible.exists() {
191        return Some(visible);
192    }
193
194    None
195}