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