1use crate::manifest::ResourceDependency;
8use crate::utils::{compute_relative_path, normalize_path_for_storage};
9use std::path::{Path, PathBuf};
10
11#[derive(Debug, Clone)]
18pub enum SourceContext {
19 Local(PathBuf),
21 Git(PathBuf),
23 Remote(String),
25}
26
27impl SourceContext {
28 pub fn local(manifest_dir: impl Into<PathBuf>) -> Self {
30 Self::Local(manifest_dir.into())
31 }
32
33 pub fn git(repo_root: impl Into<PathBuf>) -> Self {
35 Self::Git(repo_root.into())
36 }
37
38 pub fn remote(source_name: impl Into<String>) -> Self {
40 Self::Remote(source_name.into())
41 }
42
43 pub fn is_local(&self) -> bool {
45 matches!(self, Self::Local(_))
46 }
47
48 pub fn is_git(&self) -> bool {
50 matches!(self, Self::Git(_))
51 }
52
53 pub fn is_remote(&self) -> bool {
55 matches!(self, Self::Remote(_))
56 }
57}
58
59pub fn compute_canonical_name(path: &str, source_context: &SourceContext) -> String {
66 let path = Path::new(path);
67
68 let without_ext = path.with_extension("");
70
71 match source_context {
72 SourceContext::Local(manifest_dir) => {
73 let manifest_path = Path::new(manifest_dir);
77 let relative = without_ext.strip_prefix(manifest_path).unwrap_or(&without_ext);
78 normalize_path_for_storage(relative)
79 }
80 SourceContext::Git(repo_root) => {
81 if without_ext.is_absolute() {
85 compute_relative_to_repo(&without_ext, repo_root)
86 } else {
87 normalize_path_for_storage(&without_ext)
89 }
90 }
91 SourceContext::Remote(_source_name) => {
92 normalize_path_for_storage(&without_ext)
96 }
97 }
98}
99
100pub fn create_source_context_for_dependency(
105 dep: &ResourceDependency,
106 manifest_dir: Option<&Path>,
107 repo_root: Option<&Path>,
108) -> SourceContext {
109 if let Some(source_name) = dep.get_source() {
111 if let Some(repo_root) = repo_root {
113 SourceContext::git(repo_root)
114 } else {
115 SourceContext::remote(source_name)
117 }
118 } else if let Some(manifest_dir) = manifest_dir {
119 SourceContext::local(manifest_dir)
121 } else {
122 SourceContext::remote("unknown")
124 }
125}
126
127pub fn create_source_context_from_locked_resource(
131 resource: &crate::lockfile::LockedResource,
132 manifest_dir: Option<&Path>,
133) -> SourceContext {
134 if let Some(source_name) = &resource.source {
135 SourceContext::remote(source_name.clone())
137 } else {
138 if let Some(manifest_dir) = manifest_dir {
140 SourceContext::local(manifest_dir)
141 } else {
142 SourceContext::local(std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")))
144 }
145 }
146}
147
148fn compute_relative_to_repo(file_path: &Path, repo_root: &Path) -> String {
152 compute_relative_path(repo_root, file_path)
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 use std::path::Path;
161
162 #[test]
163 fn test_source_context_creation() {
164 let local = SourceContext::local("/project");
165 assert!(local.is_local());
166 assert!(!local.is_git());
167 assert!(!local.is_remote());
168
169 let git = SourceContext::git("/repo");
170 assert!(!git.is_local());
171 assert!(git.is_git());
172 assert!(!git.is_remote());
173
174 let remote = SourceContext::remote("community");
175 assert!(!remote.is_local());
176 assert!(!remote.is_git());
177 assert!(remote.is_remote());
178 }
179
180 #[test]
181 fn test_compute_canonical_name_integration() {
182 #[cfg(windows)]
184 let (project_dir, repo_dir) = ("C:\\project", "C:\\repo");
185 #[cfg(not(windows))]
186 let (project_dir, repo_dir) = ("/project", "/repo");
187
188 let local_ctx = SourceContext::local(project_dir);
190 #[cfg(windows)]
191 let local_path = "C:\\project\\local-deps\\agents\\helper.md";
192 #[cfg(not(windows))]
193 let local_path = "/project/local-deps/agents/helper.md";
194 let name = compute_canonical_name(local_path, &local_ctx);
195 assert_eq!(name, "local-deps/agents/helper");
196
197 let git_ctx = SourceContext::git(repo_dir);
199 #[cfg(windows)]
200 let git_path = "C:\\repo\\agents\\helper.md";
201 #[cfg(not(windows))]
202 let git_path = "/repo/agents/helper.md";
203 let name = compute_canonical_name(git_path, &git_ctx);
204 assert_eq!(name, "agents/helper");
205
206 let remote_ctx = SourceContext::remote("community");
208 let name = compute_canonical_name("agents/helper.md", &remote_ctx);
209 assert_eq!(name, "agents/helper");
210 }
211
212 #[test]
213 fn test_compute_canonical_name_with_already_relative_path() {
214 let local_ctx = SourceContext::local("/project");
221
222 let name = compute_canonical_name("local-deps/snippets/agents/helper.md", &local_ctx);
224 assert_eq!(name, "local-deps/snippets/agents/helper");
225
226 assert!(!name.contains(".."), "Generated name should not contain '..' sequences");
228
229 let name = compute_canonical_name("local-deps/claude/agents/rust-expert.md", &local_ctx);
231 assert_eq!(name, "local-deps/claude/agents/rust-expert");
232 assert!(!name.contains(".."));
233 }
234
235 #[test]
236 fn test_compute_canonical_name_git_context_with_relative_path() {
237 #[cfg(windows)]
241 let repo_root = "C:\\Users\\x\\.agpm\\cache\\worktrees\\repo_abc";
242 #[cfg(not(windows))]
243 let repo_root = "/Users/x/.agpm/cache/worktrees/repo_abc";
244
245 let git_ctx = SourceContext::git(repo_root);
246
247 let name = compute_canonical_name("cc-artifacts/agents/specialists/helper.md", &git_ctx);
249 assert_eq!(name, "cc-artifacts/agents/specialists/helper");
250 assert!(!name.contains(".."), "Generated name should not contain '..' sequences");
251
252 #[cfg(windows)]
254 let abs_path = "C:\\Users\\x\\.agpm\\cache\\worktrees\\repo_abc\\agents\\helper.md";
255 #[cfg(not(windows))]
256 let abs_path = "/Users/x/.agpm/cache/worktrees/repo_abc/agents/helper.md";
257
258 let name = compute_canonical_name(abs_path, &git_ctx);
259 assert_eq!(name, "agents/helper");
260 }
261
262 #[test]
264 fn test_create_source_context_for_dependency() {
265 use crate::manifest::{DetailedDependency, ResourceDependency};
266
267 let local_dep = ResourceDependency::Detailed(Box::new(DetailedDependency {
269 path: "agents/helper.md".to_string(),
270 source: None,
271 version: None,
272 branch: None,
273 rev: None,
274 command: None,
275 args: None,
276 target: None,
277 filename: None,
278 dependencies: None,
279 tool: None,
280 flatten: None,
281 install: None,
282 template_vars: None,
283 }));
284
285 let manifest_dir = Path::new("/project");
286 let ctx = create_source_context_for_dependency(&local_dep, Some(manifest_dir), None);
287 assert!(ctx.is_local());
288
289 let git_dep = ResourceDependency::Detailed(Box::new(DetailedDependency {
291 path: "agents/helper.md".to_string(),
292 source: Some("community".to_string()),
293 version: None,
294 branch: None,
295 rev: None,
296 command: None,
297 args: None,
298 target: None,
299 filename: None,
300 dependencies: None,
301 tool: None,
302 flatten: None,
303 install: None,
304 template_vars: None,
305 }));
306
307 let repo_root = Path::new("/repo");
308 let ctx =
309 create_source_context_for_dependency(&git_dep, Some(manifest_dir), Some(repo_root));
310 assert!(ctx.is_git());
311
312 let ctx = create_source_context_for_dependency(&git_dep, Some(manifest_dir), None);
314 assert!(ctx.is_remote());
315 }
316
317 #[test]
318 fn test_create_source_context_from_locked_resource() {
319 use crate::lockfile::LockedResource;
320
321 let local_resource = LockedResource {
323 name: "helper".to_string(),
324 source: None,
325 url: None,
326 path: "agents/helper.md".to_string(),
327 version: None,
328 resolved_commit: None,
329 checksum: "abc123".to_string(),
330 installed_at: "agents/helper.md".to_string(),
331 dependencies: vec![],
332 resource_type: crate::core::ResourceType::Agent,
333 tool: Some("claude-code".to_string()),
334 manifest_alias: None,
335 context_checksum: None,
336 applied_patches: std::collections::BTreeMap::new(),
337 install: None,
338 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
339 is_private: false,
340 approximate_token_count: None,
341 };
342
343 let manifest_dir = Path::new("/project");
344 let ctx = create_source_context_from_locked_resource(&local_resource, Some(manifest_dir));
345 assert!(ctx.is_local());
346
347 let mut remote_resource = local_resource.clone();
349 remote_resource.source = Some("community".to_string());
350
351 let ctx = create_source_context_from_locked_resource(&remote_resource, Some(manifest_dir));
352 assert!(ctx.is_remote());
353 assert_eq!(format!("{:?}", ctx), "Remote(\"community\")");
354 }
355}