use std::env;
use std::path::{Path, PathBuf};
use tracing::{Level, instrument};
use crate::error::{Result, SettingsError};
use crate::types::SettingsLevel;
const CLAUDE_DIR: &str = ".claude";
const SETTINGS_FILE: &str = "settings.json";
const SETTINGS_LOCAL_FILE: &str = "settings.local.json";
const SYSTEM_SETTINGS_PATH: &str = "/etc/claude-code/managed-settings.json";
#[derive(Debug, Clone)]
pub struct PathResolver {
home_override: Option<PathBuf>,
project_override: Option<PathBuf>,
}
impl Default for PathResolver {
fn default() -> Self {
Self::new()
}
}
impl PathResolver {
#[instrument(level = Level::TRACE)]
pub fn new() -> Self {
Self {
home_override: None,
project_override: None,
}
}
#[instrument(level = Level::TRACE, skip(self, home))]
pub fn with_home(mut self, home: impl Into<PathBuf>) -> Self {
self.home_override = Some(home.into());
self
}
#[instrument(level = Level::TRACE, skip(self, project))]
pub fn with_project(mut self, project: impl Into<PathBuf>) -> Self {
self.project_override = Some(project.into());
self
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn home_dir(&self) -> Result<PathBuf> {
if let Some(ref home) = self.home_override {
return Ok(home.clone());
}
env::var("HOME")
.map(PathBuf::from)
.map_err(|_| SettingsError::NoHomeDirectory)
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn project_dir(&self) -> Result<PathBuf> {
if let Some(ref project) = self.project_override {
return Ok(project.clone());
}
let cwd =
env::current_dir().map_err(|e| SettingsError::NoProjectDirectory(e.to_string()))?;
if let Some(path) = find_ancestor_with(&cwd, CLAUDE_DIR) {
return Ok(path);
}
if let Some(path) = find_ancestor_with(&cwd, ".git") {
return Ok(path);
}
Ok(cwd)
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn settings_path(&self, level: SettingsLevel) -> Result<PathBuf> {
match level {
SettingsLevel::System => Ok(PathBuf::from(SYSTEM_SETTINGS_PATH)),
SettingsLevel::User => {
let home = self.home_dir()?;
Ok(home.join(CLAUDE_DIR).join(SETTINGS_FILE))
}
SettingsLevel::Project => {
let project = self.project_dir()?;
Ok(project.join(CLAUDE_DIR).join(SETTINGS_FILE))
}
SettingsLevel::ProjectLocal => {
let project = self.project_dir()?;
Ok(project.join(CLAUDE_DIR).join(SETTINGS_LOCAL_FILE))
}
}
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn all_settings_paths(&self) -> Result<Vec<(SettingsLevel, PathBuf)>> {
let mut paths = Vec::new();
for level in SettingsLevel::all_by_priority() {
match self.settings_path(*level) {
Ok(path) => paths.push((*level, path)),
Err(SettingsError::NoHomeDirectory) if *level == SettingsLevel::User => continue,
Err(e) => return Err(e),
}
}
Ok(paths)
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn claude_dir(&self, level: SettingsLevel) -> Result<PathBuf> {
match level {
SettingsLevel::System => Ok(PathBuf::from("/etc/claude-code")),
SettingsLevel::User => {
let home = self.home_dir()?;
Ok(home.join(CLAUDE_DIR))
}
SettingsLevel::Project | SettingsLevel::ProjectLocal => {
let project = self.project_dir()?;
Ok(project.join(CLAUDE_DIR))
}
}
}
}
fn find_ancestor_with(start: &Path, name: &str) -> Option<PathBuf> {
let mut current = start.to_path_buf();
loop {
if current.join(name).exists() {
return Some(current);
}
if !current.pop() {
return None;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_path_resolver_with_overrides() {
let resolver = PathResolver::new()
.with_home("/custom/home")
.with_project("/custom/project");
assert_eq!(resolver.home_dir().unwrap(), PathBuf::from("/custom/home"));
assert_eq!(
resolver.project_dir().unwrap(),
PathBuf::from("/custom/project")
);
}
#[test]
fn test_user_settings_path() {
let resolver = PathResolver::new().with_home("/home/testuser");
let path = resolver.settings_path(SettingsLevel::User).unwrap();
assert_eq!(path, PathBuf::from("/home/testuser/.claude/settings.json"));
}
#[test]
fn test_project_settings_path() {
let resolver = PathResolver::new().with_project("/my/project");
let path = resolver.settings_path(SettingsLevel::Project).unwrap();
assert_eq!(path, PathBuf::from("/my/project/.claude/settings.json"));
}
#[test]
fn test_project_local_settings_path() {
let resolver = PathResolver::new().with_project("/my/project");
let path = resolver.settings_path(SettingsLevel::ProjectLocal).unwrap();
assert_eq!(
path,
PathBuf::from("/my/project/.claude/settings.local.json")
);
}
#[test]
fn test_system_settings_path() {
let resolver = PathResolver::new();
let path = resolver.settings_path(SettingsLevel::System).unwrap();
assert_eq!(
path,
PathBuf::from("/etc/claude-code/managed-settings.json")
);
}
#[test]
fn test_find_ancestor_with_claude_dir() {
let temp = TempDir::new().unwrap();
let project_root = temp.path();
let claude_dir = project_root.join(".claude");
std::fs::create_dir(&claude_dir).unwrap();
let nested = project_root.join("src/components");
std::fs::create_dir_all(&nested).unwrap();
let found = find_ancestor_with(&nested, ".claude");
assert_eq!(found, Some(project_root.to_path_buf()));
}
}