use crate::git::GitRepo;
use crate::manifest::{DetailedDependency, ResourceDependency};
use crate::pattern::PatternResolver;
use crate::resolver::version_resolver::PreparedSourceVersion;
use crate::utils::normalize_path_for_storage;
use anyhow::{Context, Result};
use dashmap::DashMap;
use std::path::{Path, PathBuf};
use tracing::debug;
pub async fn expand_pattern_to_concrete_deps(
dep: &ResourceDependency,
resource_type: crate::core::ResourceType,
source_manager: &crate::source::SourceManager,
cache: &crate::cache::Cache,
manifest_dir: Option<&Path>,
prepared_versions: Option<&DashMap<String, PreparedSourceVersion>>,
) -> Result<Vec<(String, ResourceDependency)>> {
let pattern = dep.get_path();
if dep.is_local() {
expand_local_pattern(dep, pattern, resource_type, manifest_dir).await
} else {
expand_remote_pattern(dep, pattern, resource_type, source_manager, cache, prepared_versions)
.await
}
}
async fn expand_local_pattern(
dep: &ResourceDependency,
pattern: &str,
resource_type: crate::core::ResourceType,
manifest_dir: Option<&Path>,
) -> Result<Vec<(String, ResourceDependency)>> {
let pattern_path = Path::new(pattern);
let (base_path, search_pattern) = if pattern_path.is_absolute() {
let components: Vec<_> = pattern_path.components().collect();
let glob_idx = components.iter().position(|c| {
let s = c.as_os_str().to_string_lossy();
s.contains('*') || s.contains('?') || s.contains('[')
});
if let Some(idx) = glob_idx {
let base_components = &components[..idx];
let pattern_components = &components[idx..];
let base: PathBuf = base_components.iter().collect();
let pattern: String = pattern_components
.iter()
.map(|c| c.as_os_str().to_string_lossy())
.collect::<Vec<_>>()
.join("/");
(base, pattern)
} else {
(PathBuf::from("."), pattern.to_string())
}
} else {
let base = manifest_dir.map(|p| p.to_path_buf()).unwrap_or_else(|| PathBuf::from("."));
(base, pattern.to_string())
};
let (tool, target, flatten) = match dep {
ResourceDependency::Detailed(d) => (d.tool.clone(), d.target.clone(), d.flatten),
_ => (None, None, None),
};
let mut concrete_deps = Vec::new();
if resource_type == crate::core::ResourceType::Skill {
let skill_matches = crate::resolver::skills::match_skill_directories(
&base_path,
&search_pattern,
None, )
.await?;
debug!("Local skill pattern '{}' matched {} directories", pattern, skill_matches.len());
for (skill_name, skill_path) in skill_matches {
let concrete_dep = ResourceDependency::Detailed(Box::new(DetailedDependency {
path: skill_path,
source: None,
version: None,
branch: None,
rev: None,
command: None,
args: None,
target: target.clone(),
filename: None,
dependencies: None,
tool: tool.clone(),
flatten,
install: None,
template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
}));
concrete_deps.push((skill_name, concrete_dep));
}
} else {
let pattern_resolver = PatternResolver::new();
let matches = pattern_resolver.resolve(&search_pattern, &base_path)?;
debug!("Pattern '{}' matched {} files", pattern, matches.len());
for matched_path in matches {
let absolute_path = base_path.join(&matched_path);
let concrete_path = normalize_path_for_storage(&absolute_path);
let source_context = if let Some(manifest_dir) = manifest_dir {
crate::resolver::source_context::SourceContext::local(manifest_dir)
} else {
crate::resolver::source_context::SourceContext::local(&base_path)
};
let dep_name = generate_dependency_name(&concrete_path, &source_context);
let concrete_dep = ResourceDependency::Detailed(Box::new(DetailedDependency {
path: concrete_path,
source: None,
version: None,
branch: None,
rev: None,
command: None,
args: None,
target: target.clone(),
filename: None,
dependencies: None,
tool: tool.clone(),
flatten,
install: None,
template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
}));
concrete_deps.push((dep_name, concrete_dep));
}
}
Ok(concrete_deps)
}
async fn expand_remote_pattern(
dep: &ResourceDependency,
pattern: &str,
resource_type: crate::core::ResourceType,
source_manager: &crate::source::SourceManager,
cache: &crate::cache::Cache,
prepared_versions: Option<&DashMap<String, PreparedSourceVersion>>,
) -> Result<Vec<(String, ResourceDependency)>> {
let source_name = dep
.get_source()
.ok_or_else(|| anyhow::anyhow!("Remote pattern dependency missing source: {}", pattern))?;
let source_url = source_manager
.get_source_url(source_name)
.with_context(|| format!("Source not found: {}", source_name))?;
let repo_path = cache
.get_or_clone_source(source_name, &source_url, dep.get_version())
.await
.with_context(|| format!("Failed to access source repository: {}", source_name))?;
let repo = GitRepo::new(&repo_path);
let version = dep.get_version().unwrap_or("HEAD");
let group_key = format!("{}::{}", source_name, version);
let (_commit_sha, worktree_path) = if let Some(prepared_map) = prepared_versions {
if let Some(prepared) = prepared_map.get(&group_key) {
(prepared.resolved_commit.clone(), prepared.worktree_path.clone())
} else {
let sha = repo.resolve_to_sha(Some(version)).await.with_context(|| {
format!("Failed to resolve version '{}' for source {}", version, source_name)
})?;
let path = cache
.get_or_create_worktree_for_sha(source_name, &source_url, &sha, Some(version))
.await
.with_context(|| {
format!("Failed to create worktree for {}@{}", source_name, version)
})?;
(sha, path)
}
} else {
let sha = repo.resolve_to_sha(Some(version)).await.with_context(|| {
format!("Failed to resolve version '{}' for source {}", version, source_name)
})?;
let path = cache
.get_or_create_worktree_for_sha(source_name, &source_url, &sha, Some(version))
.await
.with_context(|| {
format!("Failed to create worktree for {}@{}", source_name, version)
})?;
(sha, path)
};
let (tool, target, flatten) = match dep {
ResourceDependency::Detailed(d) => (d.tool.clone(), d.target.clone(), d.flatten),
_ => (None, None, None),
};
let mut concrete_deps = Vec::new();
if resource_type == crate::core::ResourceType::Skill {
let skill_matches = crate::resolver::skills::match_skill_directories(
&worktree_path,
pattern,
Some(&worktree_path),
)
.await?;
debug!(
"Remote skill pattern '{}' in {} matched {} directories",
pattern,
source_name,
skill_matches.len()
);
for (skill_name, skill_path) in skill_matches {
let concrete_dep = ResourceDependency::Detailed(Box::new(DetailedDependency {
path: skill_path,
source: Some(source_name.to_string()),
version: Some(version.to_string()),
branch: None,
rev: None,
command: None,
args: None,
target: target.clone(),
filename: None,
dependencies: None,
tool: tool.clone(),
flatten,
install: None,
template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
}));
concrete_deps.push((skill_name, concrete_dep));
}
} else {
let pattern_resolver = PatternResolver::new();
let matches = pattern_resolver.resolve(pattern, &worktree_path)?;
debug!("Remote pattern '{}' in {} matched {} files", pattern, source_name, matches.len());
for matched_path in matches {
let source_context =
crate::resolver::source_context::SourceContext::git(&worktree_path);
let dep_name =
generate_dependency_name(&matched_path.to_string_lossy(), &source_context);
let concrete_dep = ResourceDependency::Detailed(Box::new(DetailedDependency {
path: matched_path.to_string_lossy().to_string(),
source: Some(source_name.to_string()),
version: Some(version.to_string()),
branch: None,
rev: None,
command: None,
args: None,
target: target.clone(),
filename: None,
dependencies: None,
tool: tool.clone(),
flatten,
install: None,
template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
}));
concrete_deps.push((dep_name, concrete_dep));
}
}
Ok(concrete_deps)
}
pub fn generate_dependency_name(
path: &str,
source_context: &crate::resolver::source_context::SourceContext,
) -> String {
crate::resolver::source_context::compute_canonical_name(path, source_context)
}
use crate::core::ResourceType;
use std::sync::Arc;
use super::types::ResolutionCore;
pub struct PatternExpansionService {
pattern_alias_map: Arc<DashMap<(ResourceType, String), String>>,
}
impl PatternExpansionService {
pub fn new() -> Self {
Self {
pattern_alias_map: Arc::new(DashMap::new()),
}
}
pub async fn expand_pattern(
&self,
core: &ResolutionCore,
dep: &ResourceDependency,
resource_type: ResourceType,
prepared_versions: &DashMap<String, PreparedSourceVersion>,
) -> Result<Vec<(String, ResourceDependency)>> {
expand_pattern_to_concrete_deps(
dep,
resource_type,
&core.source_manager,
&core.cache,
core.manifest.manifest_dir.as_deref(),
Some(prepared_versions),
)
.await
}
pub fn get_pattern_alias(
&self,
resource_type: ResourceType,
name: &str,
) -> Option<dashmap::mapref::one::Ref<'_, (ResourceType, String), String>> {
self.pattern_alias_map.get(&(resource_type, name.to_string()))
}
pub fn add_pattern_alias(
&self,
resource_type: ResourceType,
concrete_name: String,
pattern_name: String,
) {
self.pattern_alias_map.insert((resource_type, concrete_name), pattern_name);
}
}
impl Default for PatternExpansionService {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::manifest::DetailedDependency;
use std::path::Path;
use tempfile;
use tokio::fs;
#[test]
fn test_generate_dependency_name_local_context() {
#[cfg(windows)]
let manifest_dir = Path::new("C:\\project");
#[cfg(not(windows))]
let manifest_dir = Path::new("/project");
let source_context = crate::resolver::source_context::SourceContext::local(manifest_dir);
#[cfg(windows)]
let abs_path = "C:\\project\\agents\\helper.md";
#[cfg(not(windows))]
let abs_path = "/project/agents/helper.md";
let name = generate_dependency_name(abs_path, &source_context);
assert_eq!(name, "agents/helper");
let name = generate_dependency_name("agents/helper.md", &source_context);
assert_eq!(name, "agents/helper");
#[cfg(windows)]
let nested_path = "C:\\project\\snippets\\python\\utils.md";
#[cfg(not(windows))]
let nested_path = "/project/snippets/python/utils.md";
let name = generate_dependency_name(nested_path, &source_context);
assert_eq!(name, "snippets/python/utils");
}
#[test]
fn test_generate_dependency_name_git_context() {
#[cfg(windows)]
let repo_root = Path::new("C:\\repo");
#[cfg(not(windows))]
let repo_root = Path::new("/repo");
let source_context = crate::resolver::source_context::SourceContext::git(repo_root);
#[cfg(windows)]
let repo_path = "C:\\repo\\agents\\helper.md";
#[cfg(not(windows))]
let repo_path = "/repo/agents/helper.md";
let name = generate_dependency_name(repo_path, &source_context);
assert_eq!(name, "agents/helper");
#[cfg(windows)]
let nested_path = "C:\\repo\\community\\agents\\ai\\python-assistant.md";
#[cfg(not(windows))]
let nested_path = "/repo/community/agents/ai/python-assistant.md";
let name = generate_dependency_name(nested_path, &source_context);
assert_eq!(name, "community/agents/ai/python-assistant");
}
#[test]
fn test_generate_dependency_name_remote_context() {
let source_context = crate::resolver::source_context::SourceContext::remote("community");
let name = generate_dependency_name("agents/helper.md", &source_context);
assert_eq!(name, "agents/helper");
let name = generate_dependency_name("snippets/python/async-pattern.md", &source_context);
assert_eq!(name, "snippets/python/async-pattern");
}
#[tokio::test]
async fn test_expand_local_pattern_with_source_context() {
let temp_dir = tempfile::TempDir::new().unwrap();
let manifest_dir = temp_dir.path();
fs::create_dir_all(manifest_dir.join("agents")).await.unwrap();
fs::create_dir_all(manifest_dir.join("snippets")).await.unwrap();
fs::write(manifest_dir.join("agents/helper.md"), "# Helper Agent").await.unwrap();
fs::write(manifest_dir.join("agents/assistant.md"), "# Assistant Agent").await.unwrap();
fs::write(manifest_dir.join("snippets/python.md"), "# Python Snippets").await.unwrap();
let dep = ResourceDependency::Detailed(Box::new(DetailedDependency {
path: "agents/*.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: Some(serde_json::Value::Object(serde_json::Map::new())),
}));
let result = expand_local_pattern(
&dep,
"agents/*.md",
crate::core::ResourceType::Agent,
Some(manifest_dir),
)
.await
.unwrap();
assert_eq!(result.len(), 2);
let mut names: Vec<String> = result.iter().map(|(name, _dep)| name.clone()).collect();
names.sort();
assert_eq!(names[0], "agents/assistant");
assert_eq!(names[1], "agents/helper");
for (name, expanded_dep) in &result {
assert!(expanded_dep.get_path().ends_with(".md"));
let source_context =
crate::resolver::source_context::SourceContext::local(manifest_dir);
let expected_name = generate_dependency_name(expanded_dep.get_path(), &source_context);
assert_eq!(*name, expected_name);
}
}
}