1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use skillfile_core::error::SkillfileError;
5use skillfile_core::models::{Entry, Manifest, Scope, SourceFields};
6use skillfile_sources::strategy::{content_file, is_dir_entry};
7use skillfile_sources::sync::vendor_dir_for;
8
9use crate::adapter::{adapters, PlatformAdapter};
10
11pub fn resolve_target_dir(
13 adapter_name: &str,
14 entity_type: &str,
15 scope: Scope,
16 repo_root: &Path,
17) -> Result<PathBuf, SkillfileError> {
18 let a = adapters()
19 .get(adapter_name)
20 .ok_or_else(|| SkillfileError::Manifest(format!("unknown adapter '{adapter_name}'")))?;
21 Ok(a.target_dir(entity_type, scope, repo_root))
22}
23
24pub fn installed_path(
26 entry: &Entry,
27 manifest: &Manifest,
28 repo_root: &Path,
29) -> Result<PathBuf, SkillfileError> {
30 let adapter = first_target(manifest)?;
31 let scope = manifest.install_targets[0].scope;
32 Ok(adapter.installed_path(entry, scope, repo_root))
33}
34
35pub fn installed_dir_files(
37 entry: &Entry,
38 manifest: &Manifest,
39 repo_root: &Path,
40) -> Result<HashMap<String, PathBuf>, SkillfileError> {
41 let adapter = first_target(manifest)?;
42 let scope = manifest.install_targets[0].scope;
43 Ok(adapter.installed_dir_files(entry, scope, repo_root))
44}
45
46#[must_use]
48pub fn source_path(entry: &Entry, repo_root: &Path) -> Option<PathBuf> {
49 match &entry.source {
50 SourceFields::Local { path } => Some(repo_root.join(path)),
51 SourceFields::Github { .. } | SourceFields::Url { .. } => {
52 let vdir = vendor_dir_for(entry, repo_root);
53 if is_dir_entry(entry) {
54 if vdir.exists() {
55 Some(vdir)
56 } else {
57 None
58 }
59 } else {
60 let filename = content_file(entry);
61 if filename.is_empty() {
62 None
63 } else {
64 Some(vdir.join(filename))
65 }
66 }
67 }
68 }
69}
70
71fn first_target(manifest: &Manifest) -> Result<&'static dyn PlatformAdapter, SkillfileError> {
73 if manifest.install_targets.is_empty() {
74 return Err(SkillfileError::Manifest(
75 "no install targets configured — run `skillfile install` first".into(),
76 ));
77 }
78 let t = &manifest.install_targets[0];
79 adapters()
80 .get(&t.adapter)
81 .ok_or_else(|| SkillfileError::Manifest(format!("unknown adapter '{}'", t.adapter)))
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use skillfile_core::models::{EntityType, InstallTarget};
88
89 #[test]
90 fn resolve_target_dir_global() {
91 let result =
92 resolve_target_dir("claude-code", "agent", Scope::Global, Path::new("/tmp")).unwrap();
93 assert!(result.to_string_lossy().ends_with(".claude/agents"));
94 }
95
96 #[test]
97 fn resolve_target_dir_local() {
98 let tmp = tempfile::tempdir().unwrap();
99 let result = resolve_target_dir("claude-code", "agent", Scope::Local, tmp.path()).unwrap();
100 assert_eq!(result, tmp.path().join(".claude/agents"));
101 }
102
103 #[test]
104 fn installed_path_no_targets() {
105 let entry = Entry {
106 entity_type: EntityType::Agent,
107 name: "test".into(),
108 source: SourceFields::Github {
109 owner_repo: "o/r".into(),
110 path_in_repo: "a.md".into(),
111 ref_: "main".into(),
112 },
113 };
114 let manifest = Manifest {
115 entries: vec![entry.clone()],
116 install_targets: vec![],
117 };
118 let result = installed_path(&entry, &manifest, Path::new("/tmp"));
119 assert!(result.is_err());
120 assert!(result
121 .unwrap_err()
122 .to_string()
123 .contains("no install targets"));
124 }
125
126 #[test]
127 fn installed_path_unknown_adapter() {
128 let entry = Entry {
129 entity_type: EntityType::Agent,
130 name: "test".into(),
131 source: SourceFields::Github {
132 owner_repo: "o/r".into(),
133 path_in_repo: "a.md".into(),
134 ref_: "main".into(),
135 },
136 };
137 let manifest = Manifest {
138 entries: vec![entry.clone()],
139 install_targets: vec![InstallTarget {
140 adapter: "unknown".into(),
141 scope: Scope::Global,
142 }],
143 };
144 let result = installed_path(&entry, &manifest, Path::new("/tmp"));
145 assert!(result.is_err());
146 assert!(result.unwrap_err().to_string().contains("unknown adapter"));
147 }
148
149 #[test]
150 fn installed_path_returns_correct_path() {
151 let tmp = tempfile::tempdir().unwrap();
152 let entry = Entry {
153 entity_type: EntityType::Agent,
154 name: "test".into(),
155 source: SourceFields::Github {
156 owner_repo: "o/r".into(),
157 path_in_repo: "a.md".into(),
158 ref_: "main".into(),
159 },
160 };
161 let manifest = Manifest {
162 entries: vec![entry.clone()],
163 install_targets: vec![InstallTarget {
164 adapter: "claude-code".into(),
165 scope: Scope::Local,
166 }],
167 };
168 let result = installed_path(&entry, &manifest, tmp.path()).unwrap();
169 assert_eq!(result, tmp.path().join(".claude/agents/test.md"));
170 }
171
172 #[test]
173 fn installed_dir_files_no_targets() {
174 let entry = Entry {
175 entity_type: EntityType::Agent,
176 name: "test".into(),
177 source: SourceFields::Github {
178 owner_repo: "o/r".into(),
179 path_in_repo: "agents".into(),
180 ref_: "main".into(),
181 },
182 };
183 let manifest = Manifest {
184 entries: vec![entry.clone()],
185 install_targets: vec![],
186 };
187 let result = installed_dir_files(&entry, &manifest, Path::new("/tmp"));
188 assert!(result.is_err());
189 }
190
191 #[test]
192 fn installed_dir_files_skill_dir() {
193 let tmp = tempfile::tempdir().unwrap();
194 let entry = Entry {
195 entity_type: EntityType::Skill,
196 name: "my-skill".into(),
197 source: SourceFields::Github {
198 owner_repo: "o/r".into(),
199 path_in_repo: "skills".into(),
200 ref_: "main".into(),
201 },
202 };
203 let manifest = Manifest {
204 entries: vec![entry.clone()],
205 install_targets: vec![InstallTarget {
206 adapter: "claude-code".into(),
207 scope: Scope::Local,
208 }],
209 };
210 let skill_dir = tmp.path().join(".claude/skills/my-skill");
211 std::fs::create_dir_all(&skill_dir).unwrap();
212 std::fs::write(skill_dir.join("SKILL.md"), "# Skill\n").unwrap();
213
214 let result = installed_dir_files(&entry, &manifest, tmp.path()).unwrap();
215 assert!(result.contains_key("SKILL.md"));
216 }
217
218 #[test]
219 fn installed_dir_files_agent_dir() {
220 let tmp = tempfile::tempdir().unwrap();
221 let entry = Entry {
222 entity_type: EntityType::Agent,
223 name: "my-agents".into(),
224 source: SourceFields::Github {
225 owner_repo: "o/r".into(),
226 path_in_repo: "agents".into(),
227 ref_: "main".into(),
228 },
229 };
230 let manifest = Manifest {
231 entries: vec![entry.clone()],
232 install_targets: vec![InstallTarget {
233 adapter: "claude-code".into(),
234 scope: Scope::Local,
235 }],
236 };
237 let vdir = tmp.path().join(".skillfile/cache/agents/my-agents");
239 std::fs::create_dir_all(&vdir).unwrap();
240 std::fs::write(vdir.join("a.md"), "# A\n").unwrap();
241 std::fs::write(vdir.join("b.md"), "# B\n").unwrap();
242 let agents_dir = tmp.path().join(".claude/agents");
244 std::fs::create_dir_all(&agents_dir).unwrap();
245 std::fs::write(agents_dir.join("a.md"), "# A\n").unwrap();
246 std::fs::write(agents_dir.join("b.md"), "# B\n").unwrap();
247
248 let result = installed_dir_files(&entry, &manifest, tmp.path()).unwrap();
249 assert_eq!(result.len(), 2);
250 }
251
252 #[test]
253 fn source_path_local() {
254 let tmp = tempfile::tempdir().unwrap();
255 let entry = Entry {
256 entity_type: EntityType::Skill,
257 name: "test".into(),
258 source: SourceFields::Local {
259 path: "skills/test.md".into(),
260 },
261 };
262 let result = source_path(&entry, tmp.path());
263 assert_eq!(result, Some(tmp.path().join("skills/test.md")));
264 }
265
266 #[test]
267 fn source_path_github_single() {
268 let tmp = tempfile::tempdir().unwrap();
269 let entry = Entry {
270 entity_type: EntityType::Agent,
271 name: "test".into(),
272 source: SourceFields::Github {
273 owner_repo: "o/r".into(),
274 path_in_repo: "agents/test.md".into(),
275 ref_: "main".into(),
276 },
277 };
278 let vdir = tmp.path().join(".skillfile/cache/agents/test");
279 std::fs::create_dir_all(&vdir).unwrap();
280 std::fs::write(vdir.join("test.md"), "# Test\n").unwrap();
281
282 let result = source_path(&entry, tmp.path());
283 assert_eq!(result, Some(vdir.join("test.md")));
284 }
285
286 #[test]
287 fn known_adapters_includes_claude_code() {
288 let names = super::super::adapter::known_adapters();
289 assert!(names.contains(&"claude-code"));
290 }
291}