use std::path::PathBuf;
use super::{Error, Result};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SkillsRoot {
Global {
home: PathBuf,
},
Project {
project_root: PathBuf,
},
}
impl SkillsRoot {
pub fn global() -> Result<SkillsRoot> {
Ok(SkillsRoot::Global { home: home_dir()? })
}
pub fn discovery_root(&self) -> PathBuf {
match self {
SkillsRoot::Global { home } => home.join(".claude"),
SkillsRoot::Project { project_root } => project_root.clone(),
}
}
pub fn uri_prefix(&self) -> &'static str {
match self {
SkillsRoot::Global { .. } => "global/",
SkillsRoot::Project { .. } => "",
}
}
pub fn cli_scope(global: bool, project_root: PathBuf) -> Result<Vec<SkillsRoot>> {
if global {
Ok(vec![SkillsRoot::global()?])
} else {
Ok(vec![SkillsRoot::Project { project_root }])
}
}
pub fn mcp_scope(project_root: PathBuf) -> Vec<SkillsRoot> {
let mut roots = vec![SkillsRoot::Project { project_root }];
roots.extend(SkillsRoot::global());
roots
}
}
pub(crate) fn home_dir() -> Result<PathBuf> {
let var = if cfg!(windows) { "USERPROFILE" } else { "HOME" };
std::env::var_os(var)
.filter(|v| !v.is_empty())
.map(PathBuf::from)
.ok_or_else(|| Error::Io(format!("could not resolve home directory (${var} unset)")))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_project_root_anchors_on_the_project_directory() {
let root = SkillsRoot::Project {
project_root: PathBuf::from("/tmp/proj"),
};
assert_eq!(root.discovery_root(), PathBuf::from("/tmp/proj"));
assert_eq!(root.uri_prefix(), "");
}
#[test]
fn the_global_root_anchors_on_dot_claude_not_home() {
let root = SkillsRoot::Global {
home: PathBuf::from("/home/u"),
};
assert_eq!(root.discovery_root(), PathBuf::from("/home/u/.claude"));
assert_eq!(
root.discovery_root().join("skills"),
PathBuf::from("/home/u/.claude/skills")
);
assert_eq!(root.uri_prefix(), "global/");
}
#[test]
fn scopes_are_what_each_surface_reads() {
let project = PathBuf::from("/tmp/proj");
assert_eq!(
SkillsRoot::cli_scope(false, project.clone()).expect("project scope"),
vec![SkillsRoot::Project {
project_root: project.clone()
}]
);
let global = SkillsRoot::global().expect("this machine has a home directory");
assert_eq!(
SkillsRoot::cli_scope(true, project.clone()).expect("global scope"),
vec![global.clone()]
);
assert_eq!(
SkillsRoot::mcp_scope(project.clone()),
vec![
SkillsRoot::Project {
project_root: project
},
global
],
"the MCP scope is the CLI's two scopes together, not a third rule"
);
}
}