use super::manifest::ProjectContext;
use super::manifest::SkillProjectToml;
use super::project;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct ProjectConfig {
pub project_root: PathBuf,
pub project_file_path: PathBuf,
pub skills_directory: PathBuf,
}
pub fn load_project_config(start_path: &Path) -> Result<ProjectConfig, String> {
let project_file_result = project::resolve_project_file(start_path);
if !project_file_result.found {
return Err(
"skill-project.toml not found in this directory or any parent. \
Create it at the top level of your workspace (e.g. run 'fastskill init' there), \
then run this command again."
.to_string(),
);
}
let project_file_path = project_file_result.path;
if project_file_result.context != ProjectContext::Project {
return Err(
"skill-project.toml here is for a skill (same directory as SKILL.md). \
Run install/add/list/update from the project root that has [dependencies] \
and [tool.fastskill] with skills_directory."
.to_string(),
);
}
let project = SkillProjectToml::load_from_file(&project_file_path)
.map_err(|e| format!("Failed to load skill-project.toml: {}", e))?;
let skills_directory_opt = project
.tool
.as_ref()
.and_then(|t| t.fastskill.as_ref())
.and_then(|f| f.skills_directory.as_ref());
let skills_directory = match skills_directory_opt {
Some(dir) => dir.clone(),
None => {
return Err(
"project-level skill-project.toml requires [tool.fastskill] with skills_directory. \
Run 'fastskill init --skills-dir <path>' at project root or add it manually.".to_string()
);
}
};
let project_root = project_file_path
.parent()
.unwrap_or_else(|| Path::new("."))
.to_path_buf();
let resolved_skills_directory = if skills_directory.is_absolute() {
skills_directory
} else {
project_root.join(&skills_directory)
};
Ok(ProjectConfig {
project_root,
project_file_path,
skills_directory: resolved_skills_directory,
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_load_project_config_not_found() {
let temp_dir = TempDir::new().unwrap();
let result = load_project_config(temp_dir.path());
assert!(result.is_err());
assert!(result.unwrap_err().contains("skill-project.toml not found"));
}
#[test]
fn test_load_project_config_skill_level() {
let temp_dir = TempDir::new().unwrap();
fs::write(temp_dir.path().join("SKILL.md"), "# Test Skill").unwrap();
let content = r#"
[metadata]
id = "test-skill"
version = "1.0.0"
"#;
fs::write(temp_dir.path().join("skill-project.toml"), content).unwrap();
let result = load_project_config(temp_dir.path());
assert!(result.is_err());
assert!(result.unwrap_err().contains("is for a skill"));
}
#[test]
fn test_load_project_config_missing_skills_directory() {
let temp_dir = TempDir::new().unwrap();
let content = r#"
[dependencies]
test-skill = "1.0.0"
"#;
fs::write(temp_dir.path().join("skill-project.toml"), content).unwrap();
let result = load_project_config(temp_dir.path());
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("requires [tool.fastskill] with skills_directory"));
}
#[test]
fn test_load_project_config_success_absolute() {
let temp_dir = TempDir::new().unwrap();
let skills_path = temp_dir.path().join("skills");
let content = format!(
r#"
[dependencies]
test-skill = "1.0.0"
[tool.fastskill]
skills_directory = "{}"
"#,
skills_path.display()
);
fs::write(temp_dir.path().join("skill-project.toml"), content).unwrap();
let result = load_project_config(temp_dir.path());
assert!(result.is_ok());
let config = result.unwrap();
assert_eq!(config.project_root, temp_dir.path());
assert_eq!(
config.project_file_path,
temp_dir.path().join("skill-project.toml")
);
assert_eq!(config.skills_directory, skills_path);
}
#[test]
fn test_load_project_config_success_relative() {
let temp_dir = TempDir::new().unwrap();
let content = r#"
[dependencies]
test-skill = "1.0.0"
[tool.fastskill]
skills_directory = ".claude/skills"
"#;
fs::write(temp_dir.path().join("skill-project.toml"), content).unwrap();
let result = load_project_config(temp_dir.path());
assert!(result.is_ok());
let config = result.unwrap();
assert_eq!(config.project_root, temp_dir.path());
assert_eq!(
config.project_file_path,
temp_dir.path().join("skill-project.toml")
);
assert_eq!(
config.skills_directory,
temp_dir.path().join(".claude/skills")
);
}
#[test]
fn test_load_project_config_walks_up() {
let temp_dir = TempDir::new().unwrap();
let content = r#"
[dependencies]
test-skill = "1.0.0"
[tool.fastskill]
skills_directory = ".claude/skills"
"#;
fs::write(temp_dir.path().join("skill-project.toml"), content).unwrap();
let subdir = temp_dir.path().join("subdir");
fs::create_dir_all(&subdir).unwrap();
let result = load_project_config(&subdir);
assert!(result.is_ok());
let config = result.unwrap();
assert_eq!(config.project_root, temp_dir.path());
assert_eq!(
config.project_file_path,
temp_dir.path().join("skill-project.toml")
);
assert_eq!(
config.skills_directory,
temp_dir.path().join(".claude/skills")
);
}
}