use crate::config::{find_project_skill, global_runtime_store, global_source_store};
use crate::error::{Result, SkillcError};
use crate::verbose;
use std::env;
use std::path::PathBuf;
#[derive(Debug)]
pub struct ResolvedSkill {
pub name: String,
pub source_dir: PathBuf,
pub runtime_dir: PathBuf,
}
pub fn resolve_skill(skill: &str) -> Result<ResolvedSkill> {
verbose!("resolving skill: {}", skill);
if skill.contains('/') || skill.contains('\\') {
return Err(SkillcError::SkillNotFound(format!(
"{} (use skill name, not path; run 'skc build <path>' to import first)",
skill
)));
}
if let Some(project_source) = try_project_source_store(skill)? {
verbose!(" resolved via project store: {}", project_source.display());
return finish_resolve(skill, project_source);
}
let global_path = global_source_store()?.join(skill);
verbose!(" checking global source store: {}", global_path.display());
if crate::util::is_valid_skill(&global_path) {
let source_dir = global_path.canonicalize()?;
verbose!(
" resolved via global source store: {}",
source_dir.display()
);
return finish_resolve(skill, source_dir);
} else if global_path.exists() {
return Err(SkillcError::NotAValidSkill(
global_path.to_string_lossy().to_string(),
));
}
let runtime_path = global_runtime_store()?.join(skill);
verbose!(
" checking global runtime store: {}",
runtime_path.display()
);
if crate::util::is_valid_skill(&runtime_path) {
let source_dir = runtime_path.canonicalize()?;
verbose!(
" resolved via global runtime store: {}",
source_dir.display()
);
return finish_resolve(skill, source_dir);
} else if runtime_path.exists() {
return Err(SkillcError::NotAValidSkill(
runtime_path.to_string_lossy().to_string(),
));
}
Err(SkillcError::SkillNotFound(skill.to_string()))
}
fn finish_resolve(_skill: &str, source_dir: PathBuf) -> Result<ResolvedSkill> {
let name = source_dir
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| SkillcError::InvalidPath("Cannot extract skill name".to_string()))?
.to_string();
let runtime_dir = resolve_runtime_dir_for_source(&name, &source_dir)?;
Ok(ResolvedSkill {
name,
source_dir,
runtime_dir,
})
}
fn try_project_source_store(skill: &str) -> Result<Option<PathBuf>> {
if skill.contains('/') || skill.contains('\\') {
return Ok(None);
}
if let Some((skill_path, _project_root)) = find_project_skill(skill) {
return Ok(Some(skill_path.canonicalize()?));
}
Ok(None)
}
fn resolve_runtime_dir_for_source(
skill_name: &str,
source_dir: &std::path::Path,
) -> Result<PathBuf> {
let global_source = global_source_store()?;
if source_dir.starts_with(&global_source) {
return Ok(global_runtime_store()?.join(skill_name));
}
let mut current = source_dir.parent();
while let Some(dir) = current {
if dir.file_name().is_some_and(|n| n == "skills")
&& let Some(skillc_dir) = dir.parent()
&& skillc_dir.file_name().is_some_and(|n| n == ".skillc")
&& let Some(project_root) = skillc_dir.parent()
{
return Ok(crate::util::project_skill_runtime_dir(
project_root,
skill_name,
));
}
current = dir.parent();
}
resolve_runtime_dir_from_cwd(skill_name)
}
fn resolve_runtime_dir_from_cwd(skill_name: &str) -> Result<PathBuf> {
let cwd = env::current_dir()?;
let project_config = cwd.join(".skillc").join("config.toml");
if project_config.exists() {
Ok(crate::util::project_skill_runtime_dir(&cwd, skill_name))
} else {
Ok(global_runtime_store()?.join(skill_name))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_resolve_skill_rejects_paths() {
let temp = TempDir::new().expect("failed to create temp dir");
let skill_dir = temp.path().join("my-skill");
fs::create_dir_all(&skill_dir).expect("failed to create skill dir");
fs::write(
skill_dir.join("SKILL.md"),
"---\nname: my-skill\ndescription: test\n---\n",
)
.expect("failed to write SKILL.md");
let result = resolve_skill(skill_dir.to_str().expect("path should be valid UTF-8"));
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.code(), crate::error::ErrorCode::E001);
assert!(err.to_string().contains("use skill name, not path"));
}
#[test]
fn test_resolve_skill_not_found() {
let temp = TempDir::new().expect("failed to create temp dir");
temp_env::with_var("SKILLC_HOME", Some(temp.path()), || {
let result = resolve_skill("nonexistent-skill-12345");
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.code(), crate::error::ErrorCode::E001);
});
}
#[test]
fn test_resolve_skill_rejects_relative_paths() {
let temp = TempDir::new().expect("failed to create temp dir");
temp_env::with_var("SKILLC_HOME", Some(temp.path()), || {
let result = resolve_skill("./my-skill");
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.code(), crate::error::ErrorCode::E001);
assert!(err.to_string().contains("use skill name, not path"));
});
}
#[test]
fn test_try_project_source_store_no_project() {
let temp = TempDir::new().expect("failed to create temp dir");
temp_env::with_var("SKILLC_HOME", Some(temp.path()), || {
let result = try_project_source_store("nonexistent").expect("test operation");
assert!(result.is_none());
});
}
#[test]
fn test_try_project_source_store_rejects_paths() {
let temp = TempDir::new().expect("failed to create temp dir");
temp_env::with_var("SKILLC_HOME", Some(temp.path()), || {
let result = try_project_source_store("/some/path").expect("test operation");
assert!(result.is_none());
let result = try_project_source_store("./relative").expect("test operation");
assert!(result.is_none());
});
}
}