fastskill_core/core/
project_config.rs1use super::manifest::ProjectContext;
12use super::manifest::SkillProjectToml;
13use super::project;
14use std::path::{Path, PathBuf};
15
16#[derive(Debug, Clone)]
19pub struct ProjectConfig {
20 pub project_root: PathBuf,
22 pub project_file_path: PathBuf,
24 pub skills_directory: PathBuf,
26}
27
28pub fn load_project_config(start_path: &Path) -> Result<ProjectConfig, String> {
43 let project_file_result = project::resolve_project_file(start_path);
45
46 if !project_file_result.found {
48 return Err(
49 "skill-project.toml not found in this directory or any parent. \
50 Create it at the top level of your workspace (e.g. run 'fastskill init' there), \
51 then run this command again."
52 .to_string(),
53 );
54 }
55
56 let project_file_path = project_file_result.path;
57
58 if project_file_result.context != ProjectContext::Project {
60 return Err(
61 "skill-project.toml here is for a skill (same directory as SKILL.md). \
62 Run install/add/list/update from the project root that has [dependencies] \
63 and [tool.fastskill] with skills_directory."
64 .to_string(),
65 );
66 }
67
68 let project = SkillProjectToml::load_from_file(&project_file_path)
70 .map_err(|e| format!("Failed to load skill-project.toml: {}", e))?;
71
72 let skills_directory_opt = project
74 .tool
75 .as_ref()
76 .and_then(|t| t.fastskill.as_ref())
77 .and_then(|f| f.skills_directory.as_ref());
78
79 let skills_directory = match skills_directory_opt {
80 Some(dir) => dir.clone(),
81 None => {
82 return Err(
83 "project-level skill-project.toml requires [tool.fastskill] with skills_directory. \
84 Run 'fastskill init --skills-dir <path>' at project root or add it manually.".to_string()
85 );
86 }
87 };
88
89 let project_root = project_file_path
91 .parent()
92 .unwrap_or_else(|| Path::new("."))
93 .to_path_buf();
94
95 let resolved_skills_directory = if skills_directory.is_absolute() {
96 skills_directory
97 } else {
98 project_root.join(&skills_directory)
99 };
100
101 Ok(ProjectConfig {
103 project_root,
104 project_file_path,
105 skills_directory: resolved_skills_directory,
106 })
107}
108
109#[cfg(test)]
110#[allow(clippy::unwrap_used)]
111mod tests {
112 use super::*;
113 use std::fs;
114 use tempfile::TempDir;
115
116 #[test]
117 fn test_load_project_config_not_found() {
118 let temp_dir = TempDir::new().unwrap();
119
120 let result = load_project_config(temp_dir.path());
121
122 assert!(result.is_err());
123 assert!(result.unwrap_err().contains("skill-project.toml not found"));
124 }
125
126 #[test]
127 fn test_load_project_config_skill_level() {
128 let temp_dir = TempDir::new().unwrap();
129
130 fs::write(temp_dir.path().join("SKILL.md"), "# Test Skill").unwrap();
132
133 let content = r#"
135[metadata]
136id = "test-skill"
137version = "1.0.0"
138 "#;
139 fs::write(temp_dir.path().join("skill-project.toml"), content).unwrap();
140
141 let result = load_project_config(temp_dir.path());
142
143 assert!(result.is_err());
144 assert!(result.unwrap_err().contains("is for a skill"));
145 }
146
147 #[test]
148 fn test_load_project_config_missing_skills_directory() {
149 let temp_dir = TempDir::new().unwrap();
150
151 let content = r#"
153[dependencies]
154test-skill = "1.0.0"
155 "#;
156 fs::write(temp_dir.path().join("skill-project.toml"), content).unwrap();
157
158 let result = load_project_config(temp_dir.path());
159
160 assert!(result.is_err());
161 assert!(result
162 .unwrap_err()
163 .contains("requires [tool.fastskill] with skills_directory"));
164 }
165
166 #[test]
167 fn test_load_project_config_success_absolute() {
168 let temp_dir = TempDir::new().unwrap();
169 let skills_path = temp_dir.path().join("skills");
170
171 let content = format!(
173 r#"
174[dependencies]
175test-skill = "1.0.0"
176
177[tool.fastskill]
178skills_directory = "{}"
179 "#,
180 skills_path.display()
181 );
182 fs::write(temp_dir.path().join("skill-project.toml"), content).unwrap();
183
184 let result = load_project_config(temp_dir.path());
185
186 assert!(result.is_ok());
187 let config = result.unwrap();
188 assert_eq!(config.project_root, temp_dir.path());
189 assert_eq!(
190 config.project_file_path,
191 temp_dir.path().join("skill-project.toml")
192 );
193 assert_eq!(config.skills_directory, skills_path);
194 }
195
196 #[test]
197 fn test_load_project_config_success_relative() {
198 let temp_dir = TempDir::new().unwrap();
199
200 let content = r#"
202[dependencies]
203test-skill = "1.0.0"
204
205[tool.fastskill]
206skills_directory = ".claude/skills"
207 "#;
208 fs::write(temp_dir.path().join("skill-project.toml"), content).unwrap();
209
210 let result = load_project_config(temp_dir.path());
211
212 assert!(result.is_ok());
213 let config = result.unwrap();
214 assert_eq!(config.project_root, temp_dir.path());
215 assert_eq!(
216 config.project_file_path,
217 temp_dir.path().join("skill-project.toml")
218 );
219 assert_eq!(
220 config.skills_directory,
221 temp_dir.path().join(".claude/skills")
222 );
223 }
224
225 #[test]
226 fn test_load_project_config_walks_up() {
227 let temp_dir = TempDir::new().unwrap();
228
229 let content = r#"
231[dependencies]
232test-skill = "1.0.0"
233
234[tool.fastskill]
235skills_directory = ".claude/skills"
236 "#;
237 fs::write(temp_dir.path().join("skill-project.toml"), content).unwrap();
238
239 let subdir = temp_dir.path().join("subdir");
241 fs::create_dir_all(&subdir).unwrap();
242
243 let result = load_project_config(&subdir);
245
246 assert!(result.is_ok());
247 let config = result.unwrap();
248 assert_eq!(config.project_root, temp_dir.path());
249 assert_eq!(
250 config.project_file_path,
251 temp_dir.path().join("skill-project.toml")
252 );
253 assert_eq!(
254 config.skills_directory,
255 temp_dir.path().join(".claude/skills")
256 );
257 }
258}