opencode_provider_manager/config_core/
paths.rs1use super::error::{ConfigError, Result};
18use std::path::PathBuf;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum ConfigLayer {
23 Global,
25 Project,
27 Custom,
29}
30
31#[derive(Debug, Clone)]
33pub struct ConfigPaths {
34 pub global: PathBuf,
36 pub project: Option<PathBuf>,
38 pub custom: Option<PathBuf>,
40 pub auth: PathBuf,
42 pub cache_dir: PathBuf,
44}
45
46impl ConfigPaths {
47 pub fn discover() -> Result<Self> {
55 let global = Self::global_config_path()?;
56 let project = Self::project_config_path()?;
57 let custom = std::env::var("OPENCODE_CONFIG").ok().map(PathBuf::from);
58 let auth = Self::auth_path()?;
59 let cache_dir = Self::cache_dir()?;
60
61 Ok(Self {
62 global,
63 project,
64 custom,
65 auth,
66 cache_dir,
67 })
68 }
69
70 pub fn global_config_path() -> Result<PathBuf> {
78 if let Ok(config_dir) = std::env::var("OPENCODE_CONFIG_DIR") {
80 let path = PathBuf::from(config_dir).join("opencode.json");
81 return Ok(path);
82 }
83
84 if let Some(config_home) = dirs::config_dir() {
86 let path = config_home.join("opencode").join("opencode.json");
87 if path.exists() {
88 return Ok(path);
89 }
90 }
91
92 if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME") {
94 let path = PathBuf::from(xdg_config)
95 .join("opencode")
96 .join("opencode.json");
97 if path.exists() {
98 return Ok(path);
99 }
100 }
101
102 if let Some(home) = dirs::home_dir() {
104 let fallback = home.join(".opencode.json");
105 if fallback.exists() {
106 return Ok(fallback);
107 }
108 }
109
110 dirs::config_dir()
112 .map(|d| d.join("opencode").join("opencode.json"))
113 .ok_or_else(|| {
114 ConfigError::Io(std::io::Error::new(
115 std::io::ErrorKind::NotFound,
116 "Cannot determine config directory",
117 ))
118 })
119 }
120
121 pub fn project_config_path() -> Result<Option<PathBuf>> {
126 let current_dir = std::env::current_dir().map_err(ConfigError::Io)?;
127
128 let mut dir = current_dir.as_path();
129 loop {
130 let config_path = dir.join("opencode.json");
131 if config_path.exists() {
132 return Ok(Some(config_path));
133 }
134
135 let git_dir = dir.join(".git");
137 if git_dir.exists() {
138 let root_config = dir.join("opencode.json");
140 if root_config.exists() {
141 return Ok(Some(root_config));
142 }
143 return Ok(None);
144 }
145
146 match dir.parent() {
148 Some(parent) => dir = parent,
149 None => break,
150 }
151 }
152
153 Ok(None)
154 }
155
156 pub fn auth_path() -> Result<PathBuf> {
158 dirs::data_local_dir()
160 .or_else(dirs::data_dir)
161 .map(|d| d.join("opencode").join("auth.json"))
162 .ok_or_else(|| {
163 ConfigError::Io(std::io::Error::new(
164 std::io::ErrorKind::NotFound,
165 "Cannot determine data directory for auth.json",
166 ))
167 })
168 }
169
170 pub fn cache_dir() -> Result<PathBuf> {
172 let cache = dirs::cache_dir()
173 .ok_or_else(|| {
174 ConfigError::Io(std::io::Error::new(
175 std::io::ErrorKind::NotFound,
176 "Cannot determine cache directory",
177 ))
178 })
179 .map(|p| p.join("opencode-provider-manager"))?;
180 Ok(cache)
181 }
182
183 pub fn managed_config_path() -> Option<PathBuf> {
185 #[cfg(target_os = "macos")]
186 {
187 Some(PathBuf::from(
188 "/Library/Application Support/opencode/opencode.json",
189 ))
190 }
191
192 #[cfg(target_os = "linux")]
193 {
194 Some(PathBuf::from("/etc/opencode/opencode.json"))
195 }
196
197 #[cfg(target_os = "windows")]
198 {
199 std::env::var("ProgramData")
200 .ok()
201 .map(|p| PathBuf::from(p).join("opencode").join("opencode.json"))
202 }
203
204 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
205 {
206 None
207 }
208 }
209
210 pub fn path_for_layer(&self, layer: ConfigLayer) -> Option<&PathBuf> {
212 match layer {
213 ConfigLayer::Global => Some(&self.global),
214 ConfigLayer::Project => self.project.as_ref(),
215 ConfigLayer::Custom => self.custom.as_ref(),
216 }
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn test_global_config_path_returns_something() {
226 let path = ConfigPaths::global_config_path().unwrap();
228 assert!(path.to_string_lossy().contains("opencode"));
229 }
230
231 #[test]
232 fn test_auth_path_returns_something() {
233 let path = ConfigPaths::auth_path().unwrap();
234 assert!(path.to_string_lossy().contains("opencode"));
235 assert!(path.to_string_lossy().contains("auth.json"));
236 }
237
238 #[test]
239 fn test_cache_dir_returns_something() {
240 let path = ConfigPaths::cache_dir().unwrap();
241 assert!(path.to_string_lossy().contains("opencode-provider-manager"));
242 }
243
244 #[test]
245 fn test_config_layer_enum_values() {
246 assert_eq!(ConfigLayer::Global, ConfigLayer::Global);
247 assert_eq!(ConfigLayer::Project, ConfigLayer::Project);
248 assert_eq!(ConfigLayer::Custom, ConfigLayer::Custom);
249 }
250
251 #[test]
252 fn test_discover_returns_structure() {
253 let paths = ConfigPaths::discover().unwrap();
254 assert!(!paths.global.to_string_lossy().is_empty());
255 assert!(!paths.auth.to_string_lossy().is_empty());
256 assert!(!paths.cache_dir.to_string_lossy().is_empty());
257 }
258
259 #[test]
260 fn test_path_for_layer() {
261 let paths = ConfigPaths::discover().unwrap();
262 assert!(paths.path_for_layer(ConfigLayer::Global).is_some());
263 assert!(
265 paths.path_for_layer(ConfigLayer::Custom).is_none()
266 || paths.path_for_layer(ConfigLayer::Custom).is_some()
267 );
268 }
269}