use super::error::{ConfigError, Result};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ConfigLayer {
Global,
Project,
Custom,
}
#[derive(Debug, Clone)]
pub struct ConfigPaths {
pub global: PathBuf,
pub project: Option<PathBuf>,
pub custom: Option<PathBuf>,
pub auth: PathBuf,
pub cache_dir: PathBuf,
}
impl ConfigPaths {
pub fn discover() -> Result<Self> {
let global = Self::global_config_path()?;
let project = Self::project_config_path()?;
let custom = std::env::var("OPENCODE_CONFIG").ok().map(PathBuf::from);
let auth = Self::auth_path()?;
let cache_dir = Self::cache_dir()?;
Ok(Self {
global,
project,
custom,
auth,
cache_dir,
})
}
pub fn global_config_path() -> Result<PathBuf> {
if let Ok(config_dir) = std::env::var("OPENCODE_CONFIG_DIR") {
let path = PathBuf::from(config_dir).join("opencode.json");
return Ok(path);
}
if let Some(config_home) = dirs::config_dir() {
let path = config_home.join("opencode").join("opencode.json");
if path.exists() {
return Ok(path);
}
}
if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME") {
let path = PathBuf::from(xdg_config)
.join("opencode")
.join("opencode.json");
if path.exists() {
return Ok(path);
}
}
if let Some(home) = dirs::home_dir() {
let fallback = home.join(".opencode.json");
if fallback.exists() {
return Ok(fallback);
}
}
dirs::config_dir()
.map(|d| d.join("opencode").join("opencode.json"))
.ok_or_else(|| {
ConfigError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Cannot determine config directory",
))
})
}
pub fn project_config_path() -> Result<Option<PathBuf>> {
let current_dir = std::env::current_dir().map_err(ConfigError::Io)?;
let mut dir = current_dir.as_path();
loop {
let config_path = dir.join("opencode.json");
if config_path.exists() {
return Ok(Some(config_path));
}
let git_dir = dir.join(".git");
if git_dir.exists() {
let root_config = dir.join("opencode.json");
if root_config.exists() {
return Ok(Some(root_config));
}
return Ok(None);
}
match dir.parent() {
Some(parent) => dir = parent,
None => break,
}
}
Ok(None)
}
pub fn auth_path() -> Result<PathBuf> {
dirs::data_local_dir()
.or_else(dirs::data_dir)
.map(|d| d.join("opencode").join("auth.json"))
.ok_or_else(|| {
ConfigError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Cannot determine data directory for auth.json",
))
})
}
pub fn cache_dir() -> Result<PathBuf> {
let cache = dirs::cache_dir()
.ok_or_else(|| {
ConfigError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Cannot determine cache directory",
))
})
.map(|p| p.join("opencode-provider-manager"))?;
Ok(cache)
}
pub fn managed_config_path() -> Option<PathBuf> {
#[cfg(target_os = "macos")]
{
Some(PathBuf::from(
"/Library/Application Support/opencode/opencode.json",
))
}
#[cfg(target_os = "linux")]
{
Some(PathBuf::from("/etc/opencode/opencode.json"))
}
#[cfg(target_os = "windows")]
{
std::env::var("ProgramData")
.ok()
.map(|p| PathBuf::from(p).join("opencode").join("opencode.json"))
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
None
}
}
pub fn path_for_layer(&self, layer: ConfigLayer) -> Option<&PathBuf> {
match layer {
ConfigLayer::Global => Some(&self.global),
ConfigLayer::Project => self.project.as_ref(),
ConfigLayer::Custom => self.custom.as_ref(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_global_config_path_returns_something() {
let path = ConfigPaths::global_config_path().unwrap();
assert!(path.to_string_lossy().contains("opencode"));
}
#[test]
fn test_auth_path_returns_something() {
let path = ConfigPaths::auth_path().unwrap();
assert!(path.to_string_lossy().contains("opencode"));
assert!(path.to_string_lossy().contains("auth.json"));
}
#[test]
fn test_cache_dir_returns_something() {
let path = ConfigPaths::cache_dir().unwrap();
assert!(path.to_string_lossy().contains("opencode-provider-manager"));
}
#[test]
fn test_config_layer_enum_values() {
assert_eq!(ConfigLayer::Global, ConfigLayer::Global);
assert_eq!(ConfigLayer::Project, ConfigLayer::Project);
assert_eq!(ConfigLayer::Custom, ConfigLayer::Custom);
}
#[test]
fn test_discover_returns_structure() {
let paths = ConfigPaths::discover().unwrap();
assert!(!paths.global.to_string_lossy().is_empty());
assert!(!paths.auth.to_string_lossy().is_empty());
assert!(!paths.cache_dir.to_string_lossy().is_empty());
}
#[test]
fn test_path_for_layer() {
let paths = ConfigPaths::discover().unwrap();
assert!(paths.path_for_layer(ConfigLayer::Global).is_some());
assert!(
paths.path_for_layer(ConfigLayer::Custom).is_none()
|| paths.path_for_layer(ConfigLayer::Custom).is_some()
);
}
}