agpm_cli/resolver/
source_context.rs1use 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) => compute_relative_to_repo(&without_ext, repo_root),
81 SourceContext::Remote(_source_name) => {
82 normalize_path_for_storage(&without_ext)
86 }
87 }
88}
89
90pub fn create_source_context_for_dependency(
95 dep: &ResourceDependency,
96 manifest_dir: Option<&Path>,
97 repo_root: Option<&Path>,
98) -> SourceContext {
99 if let Some(source_name) = dep.get_source() {
101 if let Some(repo_root) = repo_root {
103 SourceContext::git(repo_root)
104 } else {
105 SourceContext::remote(source_name)
107 }
108 } else if let Some(manifest_dir) = manifest_dir {
109 SourceContext::local(manifest_dir)
111 } else {
112 SourceContext::remote("unknown")
114 }
115}
116
117pub fn create_source_context_from_locked_resource(
121 resource: &crate::lockfile::LockedResource,
122 manifest_dir: Option<&Path>,
123) -> SourceContext {
124 if let Some(source_name) = &resource.source {
125 SourceContext::remote(source_name.clone())
127 } else {
128 if let Some(manifest_dir) = manifest_dir {
130 SourceContext::local(manifest_dir)
131 } else {
132 SourceContext::local(std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")))
134 }
135 }
136}
137
138fn compute_relative_to_repo(file_path: &Path, repo_root: &Path) -> String {
142 compute_relative_path(repo_root, file_path)
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 use std::path::Path;
151
152 #[test]
153 fn test_source_context_creation() {
154 let local = SourceContext::local("/project");
155 assert!(local.is_local());
156 assert!(!local.is_git());
157 assert!(!local.is_remote());
158
159 let git = SourceContext::git("/repo");
160 assert!(!git.is_local());
161 assert!(git.is_git());
162 assert!(!git.is_remote());
163
164 let remote = SourceContext::remote("community");
165 assert!(!remote.is_local());
166 assert!(!remote.is_git());
167 assert!(remote.is_remote());
168 }
169
170 #[test]
171 fn test_compute_canonical_name_integration() {
172 let local_ctx = SourceContext::local("/project");
174 let name = compute_canonical_name("/project/local-deps/agents/helper.md", &local_ctx);
175 assert_eq!(name, "local-deps/agents/helper");
176
177 let git_ctx = SourceContext::git("/repo");
179 let name = compute_canonical_name("/repo/agents/helper.md", &git_ctx);
180 assert_eq!(name, "agents/helper");
181
182 let remote_ctx = SourceContext::remote("community");
184 let name = compute_canonical_name("agents/helper.md", &remote_ctx);
185 assert_eq!(name, "agents/helper");
186 }
187
188 #[test]
189 fn test_compute_canonical_name_with_already_relative_path() {
190 let local_ctx = SourceContext::local("/project");
197
198 let name = compute_canonical_name("local-deps/snippets/agents/helper.md", &local_ctx);
200 assert_eq!(name, "local-deps/snippets/agents/helper");
201
202 assert!(!name.contains(".."), "Generated name should not contain '..' sequences");
204
205 let name = compute_canonical_name("local-deps/claude/agents/rust-expert.md", &local_ctx);
207 assert_eq!(name, "local-deps/claude/agents/rust-expert");
208 assert!(!name.contains(".."));
209 }
210
211 #[test]
213 fn test_create_source_context_for_dependency() {
214 use crate::manifest::{DetailedDependency, ResourceDependency};
215
216 let local_dep = ResourceDependency::Detailed(Box::new(DetailedDependency {
218 path: "agents/helper.md".to_string(),
219 source: None,
220 version: None,
221 branch: None,
222 rev: None,
223 command: None,
224 args: None,
225 target: None,
226 filename: None,
227 dependencies: None,
228 tool: None,
229 flatten: None,
230 install: None,
231 template_vars: None,
232 }));
233
234 let manifest_dir = Path::new("/project");
235 let ctx = create_source_context_for_dependency(&local_dep, Some(manifest_dir), None);
236 assert!(ctx.is_local());
237
238 let git_dep = ResourceDependency::Detailed(Box::new(DetailedDependency {
240 path: "agents/helper.md".to_string(),
241 source: Some("community".to_string()),
242 version: None,
243 branch: None,
244 rev: None,
245 command: None,
246 args: None,
247 target: None,
248 filename: None,
249 dependencies: None,
250 tool: None,
251 flatten: None,
252 install: None,
253 template_vars: None,
254 }));
255
256 let repo_root = Path::new("/repo");
257 let ctx =
258 create_source_context_for_dependency(&git_dep, Some(manifest_dir), Some(repo_root));
259 assert!(ctx.is_git());
260
261 let ctx = create_source_context_for_dependency(&git_dep, Some(manifest_dir), None);
263 assert!(ctx.is_remote());
264 }
265
266 #[test]
267 fn test_create_source_context_from_locked_resource() {
268 use crate::lockfile::LockedResource;
269
270 let local_resource = LockedResource {
272 name: "helper".to_string(),
273 source: None,
274 url: None,
275 path: "agents/helper.md".to_string(),
276 version: None,
277 resolved_commit: None,
278 checksum: "abc123".to_string(),
279 installed_at: "agents/helper.md".to_string(),
280 dependencies: vec![],
281 resource_type: crate::core::ResourceType::Agent,
282 tool: Some("claude-code".to_string()),
283 manifest_alias: None,
284 context_checksum: None,
285 applied_patches: std::collections::BTreeMap::new(),
286 install: None,
287 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
288 };
289
290 let manifest_dir = Path::new("/project");
291 let ctx = create_source_context_from_locked_resource(&local_resource, Some(manifest_dir));
292 assert!(ctx.is_local());
293
294 let mut remote_resource = local_resource.clone();
296 remote_resource.source = Some("community".to_string());
297
298 let ctx = create_source_context_from_locked_resource(&remote_resource, Some(manifest_dir));
299 assert!(ctx.is_remote());
300 assert_eq!(format!("{:?}", ctx), "Remote(\"community\")");
301 }
302}