返回 CodeWhale
paths.rs
根目录 / crates / tui / src / config / paths.rs
1 //! Filesystem path resolution helpers for config/cache/workspace locations.
2 //!
3 //! Pure path-building helpers extracted verbatim from `config.rs`. They depend
4 //! only on `std`, `codewhale-paths`, and `shellexpand` plus one another, so they
5 //! form a clean leaf. `config.rs` pulls them back in (`use paths::{...}`) for the
6 //! workspace-trust and config-loading logic that stays there, and re-exports
7 //! the two `pub(crate)` entry points (`effective_home_dir`, `expand_path`) so
8 //! external `crate::config::` callers resolve unchanged (#3311).
9 //!
10 //! Visibility note: helpers that were file-private `fn` in `config.rs` are
11 //! `pub(crate)` here purely so the parent module can name them; none are
12 //! re-exported publicly, so the crate's external surface is unchanged.
13
14 use std::path::{Path, PathBuf};
15
16 /// Re-exported so `config::effective_home_dir` and every `use paths::{...}`
17 /// caller resolve unchanged. It lives in `home.rs` because this module is not
18 /// includable from an integration test binary — see that file's header.
19 pub(crate) use super::home::effective_home_dir;
20
21 pub(crate) fn default_config_path() -> anyhow::Result<PathBuf> {
22 try_default_config_path()
23 }
24
25 pub(crate) fn try_default_config_path() -> anyhow::Result<PathBuf> {
26 #[cfg(test)]
27 {
28 with_test_state_path(try_default_config_path_from_environment, || {
29 Ok(crate::test_support::unsealed_test_state_root()
30 .join(codewhale_config::CONFIG_FILE_NAME))
31 })
32 }
33
34 #[cfg(not(test))]
35 try_default_config_path_from_environment()
36 }
37
38 fn try_default_config_path_from_environment() -> anyhow::Result<PathBuf> {
39 codewhale_config::resolve_config_path(None)
40 }
41
42 /// Holding [`lock_test_env`] is not enough to read the process environment:
43 /// many tests take that lock only to serialize unrelated variables, and
44 /// trusting it routed them at a populated `~/.codewhale/config.toml` (#5355,
45 /// #5359). Settings already requires a sealed `EnvVarGuard`; config paths
46 /// must use the same gate.
47 #[cfg(test)]
48 fn with_test_state_path<T>(
49 from_environment: impl FnOnce() -> T,
50 isolated: impl FnOnce() -> T,
51 ) -> T {
52 let honor_guarded_environment = crate::test_support::guarded_environment_provides_state_paths();
53 crate::test_support::with_test_env_lock(|| {
54 if honor_guarded_environment {
55 from_environment()
56 } else {
57 isolated()
58 }
59 })
60 }
61
62 pub(crate) fn codewhale_home_dir() -> Result<Option<PathBuf>, codewhale_paths::PathOverrideError> {
63 codewhale_paths::codewhale_home_override()
64 }
65
66 /// The user-global config document: `$CODEWHALE_HOME/config.toml` when an
67 /// explicit home is set, otherwise `~/.codewhale/config.toml` (falling back to
68 /// the legacy `~/.deepseek/config.toml` only when that file already exists).
69 ///
70 /// Credential writes are rerouted here when the ambient config path resolves
71 /// to a workspace-scoped document (#5045, #5193); non-credential settings keep
72 /// the ambient scoping.
73 pub(crate) fn home_config_path() -> Option<PathBuf> {
74 #[cfg(test)]
75 {
76 with_test_state_path(home_config_path_from_environment, || {
77 Some(
78 crate::test_support::unsealed_test_state_root()
79 .join(codewhale_config::CONFIG_FILE_NAME),
80 )
81 })
82 }
83
84 #[cfg(not(test))]
85 home_config_path_from_environment()
86 }
87
88 /// Compare the physical user-global document using the same parent path
89 /// normalization and file-symlink rejection as ConfigStore.
90 pub(crate) fn is_home_config_path(path: &Path) -> bool {
91 let Some(home) = home_config_path() else {
92 return false;
93 };
94 match (
95 codewhale_config::resolve_config_path(Some(home)),
96 codewhale_config::resolve_config_path(Some(path.to_path_buf())),
97 ) {
98 (Ok(home), Ok(path)) => home == path,
99 _ => false,
100 }
101 }
102
103 fn home_config_path_from_environment() -> Option<PathBuf> {
104 match codewhale_home_dir() {
105 Ok(Some(home)) => return Some(home.join(codewhale_config::CONFIG_FILE_NAME)),
106 Ok(None) => {}
107 Err(error) => {
108 tracing::error!(
109 error = %error,
110 "invalid Codewhale home override; refusing to substitute a different config path"
111 );
112 return None;
113 }
114 }
115
116 effective_home_dir().map(|home| {
117 let primary = home.join(".codewhale").join("config.toml");
118 if primary.exists() {
119 return primary;
120 }
121 let legacy = home.join(".deepseek").join("config.toml");
122 if legacy.exists() {
123 return legacy;
124 }
125 primary
126 })
127 }
128
129 pub(crate) fn workspace_config_key(workspace: &Path) -> String {
130 canonicalize_or_keep(workspace)
131 .to_string_lossy()
132 .into_owned()
133 }
134
135 pub(crate) fn canonicalize_or_keep(path: &Path) -> PathBuf {
136 path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
137 }
138
139 pub(crate) fn env_config_path() -> Result<Option<PathBuf>, codewhale_paths::PathOverrideError> {
140 #[cfg(test)]
141 {
142 with_test_state_path(env_config_path_unlocked, || Ok(None))
143 }
144 #[cfg(not(test))]
145 {
146 env_config_path_unlocked()
147 }
148 }
149
150 fn env_config_path_unlocked() -> Result<Option<PathBuf>, codewhale_paths::PathOverrideError> {
151 codewhale_paths::config_path_override()
152 }
153
154 pub(crate) fn expand_pathbuf(path: PathBuf) -> PathBuf {
155 if let Some(raw) = path.to_str() {
156 return expand_path(raw);
157 }
158 path
159 }
160
161 pub(crate) fn default_managed_config_path() -> Option<PathBuf> {
162 #[cfg(unix)]
163 {
164 Some(PathBuf::from("/etc/deepseek/managed_config.toml"))
165 }
166 #[cfg(not(unix))]
167 {
168 effective_home_dir().map(|home| {
169 let primary = home.join(".codewhale").join("managed_config.toml");
170 if primary.exists() {
171 return primary;
172 }
173 home.join(".deepseek").join("managed_config.toml")
174 })
175 }
176 }
177
178 pub(crate) fn default_requirements_path() -> Option<PathBuf> {
179 #[cfg(unix)]
180 {
181 Some(PathBuf::from("/etc/deepseek/requirements.toml"))
182 }
183 #[cfg(not(unix))]
184 {
185 effective_home_dir().map(|home| {
186 let primary = home.join(".codewhale").join("requirements.toml");
187 if primary.exists() {
188 return primary;
189 }
190 home.join(".deepseek").join("requirements.toml")
191 })
192 }
193 }
194
195 pub(crate) fn expand_path(path: &str) -> PathBuf {
196 if let Some(stripped) = path.strip_prefix('~')
197 && (stripped.is_empty() || stripped.starts_with('/') || stripped.starts_with('\\'))
198 && let Some(mut home) = effective_home_dir()
199 {
200 let suffix = stripped.trim_start_matches(['/', '\\']);
201 if !suffix.is_empty() {
202 home.push(suffix);
203 }
204 return home;
205 }
206
207 let expanded = shellexpand::tilde(path);
208 PathBuf::from(expanded.as_ref())
209 }
210
211 pub(crate) fn default_skills_dir() -> Option<PathBuf> {
212 default_user_state_path("skills")
213 }
214
215 pub(crate) fn default_mcp_config_path() -> Option<PathBuf> {
216 default_user_state_path("mcp.json")
217 }
218
219 pub(crate) fn default_notes_path() -> Option<PathBuf> {
220 default_user_state_path("notes.txt")
221 }
222
223 pub(crate) fn default_memory_path() -> Option<PathBuf> {
224 default_user_state_path("memory.md")
225 }
226
227 fn default_user_state_path(name: &str) -> Option<PathBuf> {
228 #[cfg(test)]
229 {
230 with_test_state_path(
231 || default_user_state_path_from_environment(name),
232 || Some(crate::test_support::unsealed_test_state_root().join(name)),
233 )
234 }
235
236 #[cfg(not(test))]
237 default_user_state_path_from_environment(name)
238 }
239
240 fn default_user_state_path_from_environment(name: &str) -> Option<PathBuf> {
241 match codewhale_home_dir() {
242 Ok(Some(home)) => return Some(home.join(name)),
243 Ok(None) => {}
244 Err(error) => {
245 tracing::error!(
246 error = %error,
247 "invalid Codewhale home override; refusing to substitute a different state root"
248 );
249 return None;
250 }
251 }
252 effective_home_dir().map(|home| {
253 let primary = home.join(".codewhale").join(name);
254 if primary.exists() {
255 return primary;
256 }
257 let legacy = home.join(".deepseek").join(name);
258 if legacy.exists() {
259 return legacy;
260 }
261 primary
262 })
263 }
264
264 lines RUST