use crate::manifest::ResourceDependency;
use crate::utils::{compute_relative_path, normalize_path_for_storage};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub enum SourceContext {
Local(PathBuf),
Git(PathBuf),
Remote(String),
}
impl SourceContext {
pub fn local(manifest_dir: impl Into<PathBuf>) -> Self {
Self::Local(manifest_dir.into())
}
pub fn git(repo_root: impl Into<PathBuf>) -> Self {
Self::Git(repo_root.into())
}
pub fn remote(source_name: impl Into<String>) -> Self {
Self::Remote(source_name.into())
}
pub fn is_local(&self) -> bool {
matches!(self, Self::Local(_))
}
pub fn is_git(&self) -> bool {
matches!(self, Self::Git(_))
}
pub fn is_remote(&self) -> bool {
matches!(self, Self::Remote(_))
}
}
pub fn compute_canonical_name(path: &str, source_context: &SourceContext) -> String {
let path = Path::new(path);
let without_ext = path.with_extension("");
match source_context {
SourceContext::Local(manifest_dir) => {
let manifest_path = Path::new(manifest_dir);
let relative = without_ext.strip_prefix(manifest_path).unwrap_or(&without_ext);
normalize_path_for_storage(relative)
}
SourceContext::Git(repo_root) => {
if without_ext.is_absolute() {
compute_relative_to_repo(&without_ext, repo_root)
} else {
normalize_path_for_storage(&without_ext)
}
}
SourceContext::Remote(_source_name) => {
normalize_path_for_storage(&without_ext)
}
}
}
pub fn create_source_context_for_dependency(
dep: &ResourceDependency,
manifest_dir: Option<&Path>,
repo_root: Option<&Path>,
) -> SourceContext {
if let Some(source_name) = dep.get_source() {
if let Some(repo_root) = repo_root {
SourceContext::git(repo_root)
} else {
SourceContext::remote(source_name)
}
} else if let Some(manifest_dir) = manifest_dir {
SourceContext::local(manifest_dir)
} else {
SourceContext::remote("unknown")
}
}
pub fn create_source_context_from_locked_resource(
resource: &crate::lockfile::LockedResource,
manifest_dir: Option<&Path>,
) -> SourceContext {
if let Some(source_name) = &resource.source {
SourceContext::remote(source_name.clone())
} else {
if let Some(manifest_dir) = manifest_dir {
SourceContext::local(manifest_dir)
} else {
SourceContext::local(std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")))
}
}
}
fn compute_relative_to_repo(file_path: &Path, repo_root: &Path) -> String {
compute_relative_path(repo_root, file_path)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn test_source_context_creation() {
let local = SourceContext::local("/project");
assert!(local.is_local());
assert!(!local.is_git());
assert!(!local.is_remote());
let git = SourceContext::git("/repo");
assert!(!git.is_local());
assert!(git.is_git());
assert!(!git.is_remote());
let remote = SourceContext::remote("community");
assert!(!remote.is_local());
assert!(!remote.is_git());
assert!(remote.is_remote());
}
#[test]
fn test_compute_canonical_name_integration() {
#[cfg(windows)]
let (project_dir, repo_dir) = ("C:\\project", "C:\\repo");
#[cfg(not(windows))]
let (project_dir, repo_dir) = ("/project", "/repo");
let local_ctx = SourceContext::local(project_dir);
#[cfg(windows)]
let local_path = "C:\\project\\local-deps\\agents\\helper.md";
#[cfg(not(windows))]
let local_path = "/project/local-deps/agents/helper.md";
let name = compute_canonical_name(local_path, &local_ctx);
assert_eq!(name, "local-deps/agents/helper");
let git_ctx = SourceContext::git(repo_dir);
#[cfg(windows)]
let git_path = "C:\\repo\\agents\\helper.md";
#[cfg(not(windows))]
let git_path = "/repo/agents/helper.md";
let name = compute_canonical_name(git_path, &git_ctx);
assert_eq!(name, "agents/helper");
let remote_ctx = SourceContext::remote("community");
let name = compute_canonical_name("agents/helper.md", &remote_ctx);
assert_eq!(name, "agents/helper");
}
#[test]
fn test_compute_canonical_name_with_already_relative_path() {
let local_ctx = SourceContext::local("/project");
let name = compute_canonical_name("local-deps/snippets/agents/helper.md", &local_ctx);
assert_eq!(name, "local-deps/snippets/agents/helper");
assert!(!name.contains(".."), "Generated name should not contain '..' sequences");
let name = compute_canonical_name("local-deps/claude/agents/rust-expert.md", &local_ctx);
assert_eq!(name, "local-deps/claude/agents/rust-expert");
assert!(!name.contains(".."));
}
#[test]
fn test_compute_canonical_name_git_context_with_relative_path() {
#[cfg(windows)]
let repo_root = "C:\\Users\\x\\.agpm\\cache\\worktrees\\repo_abc";
#[cfg(not(windows))]
let repo_root = "/Users/x/.agpm/cache/worktrees/repo_abc";
let git_ctx = SourceContext::git(repo_root);
let name = compute_canonical_name("cc-artifacts/agents/specialists/helper.md", &git_ctx);
assert_eq!(name, "cc-artifacts/agents/specialists/helper");
assert!(!name.contains(".."), "Generated name should not contain '..' sequences");
#[cfg(windows)]
let abs_path = "C:\\Users\\x\\.agpm\\cache\\worktrees\\repo_abc\\agents\\helper.md";
#[cfg(not(windows))]
let abs_path = "/Users/x/.agpm/cache/worktrees/repo_abc/agents/helper.md";
let name = compute_canonical_name(abs_path, &git_ctx);
assert_eq!(name, "agents/helper");
}
#[test]
fn test_create_source_context_for_dependency() {
use crate::manifest::{DetailedDependency, ResourceDependency};
let local_dep = ResourceDependency::Detailed(Box::new(DetailedDependency {
path: "agents/helper.md".to_string(),
source: None,
version: None,
branch: None,
rev: None,
command: None,
args: None,
target: None,
filename: None,
dependencies: None,
tool: None,
flatten: None,
install: None,
template_vars: None,
}));
let manifest_dir = Path::new("/project");
let ctx = create_source_context_for_dependency(&local_dep, Some(manifest_dir), None);
assert!(ctx.is_local());
let git_dep = ResourceDependency::Detailed(Box::new(DetailedDependency {
path: "agents/helper.md".to_string(),
source: Some("community".to_string()),
version: None,
branch: None,
rev: None,
command: None,
args: None,
target: None,
filename: None,
dependencies: None,
tool: None,
flatten: None,
install: None,
template_vars: None,
}));
let repo_root = Path::new("/repo");
let ctx =
create_source_context_for_dependency(&git_dep, Some(manifest_dir), Some(repo_root));
assert!(ctx.is_git());
let ctx = create_source_context_for_dependency(&git_dep, Some(manifest_dir), None);
assert!(ctx.is_remote());
}
#[test]
fn test_create_source_context_from_locked_resource() {
use crate::lockfile::LockedResource;
let local_resource = LockedResource {
name: "helper".to_string(),
source: None,
url: None,
path: "agents/helper.md".to_string(),
version: None,
resolved_commit: None,
checksum: "abc123".to_string(),
installed_at: "agents/helper.md".to_string(),
dependencies: vec![],
resource_type: crate::core::ResourceType::Agent,
tool: Some("claude-code".to_string()),
manifest_alias: None,
context_checksum: None,
applied_patches: std::collections::BTreeMap::new(),
install: None,
variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
is_private: false,
approximate_token_count: None,
};
let manifest_dir = Path::new("/project");
let ctx = create_source_context_from_locked_resource(&local_resource, Some(manifest_dir));
assert!(ctx.is_local());
let mut remote_resource = local_resource.clone();
remote_resource.source = Some("community".to_string());
let ctx = create_source_context_from_locked_resource(&remote_resource, Some(manifest_dir));
assert!(ctx.is_remote());
assert_eq!(format!("{:?}", ctx), "Remote(\"community\")");
}
}