use std::collections::HashMap;
use std::sync::Arc;
use dashmap::DashMap;
use crate::cache::Cache;
use crate::core::ResourceType;
use crate::core::operation_context::OperationContext;
use crate::lockfile::lockfile_dependency_ref::LockfileDependencyRef;
use crate::manifest::{Manifest, ResourceDependency};
use crate::source::SourceManager;
use crate::version::conflict::ConflictDetector;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResolutionMode {
Version,
GitRef,
}
impl ResolutionMode {
pub fn from_dependency(dep: &crate::manifest::ResourceDependency) -> Self {
use crate::manifest::ResourceDependency;
match dep {
ResourceDependency::Simple(_) => Self::Version, ResourceDependency::Detailed(d) => {
if d.branch.is_some() || d.rev.is_some() {
Self::GitRef
} else {
Self::Version
}
}
}
}
}
pub struct ResolutionCore {
pub manifest: Manifest,
pub cache: Cache,
pub source_manager: SourceManager,
pub operation_context: Option<Arc<OperationContext>>,
}
impl ResolutionCore {
pub fn new(
manifest: Manifest,
cache: Cache,
source_manager: SourceManager,
operation_context: Option<Arc<OperationContext>>,
) -> Self {
Self {
manifest,
cache,
source_manager,
operation_context,
}
}
pub fn manifest(&self) -> &Manifest {
&self.manifest
}
pub fn cache(&self) -> &Cache {
&self.cache
}
pub fn source_manager(&self) -> &SourceManager {
&self.source_manager
}
pub fn operation_context(&self) -> Option<&Arc<OperationContext>> {
self.operation_context.as_ref()
}
}
pub type DependencyKey = (ResourceType, String, Option<String>, Option<String>, String);
#[derive(Copy, Clone)]
pub struct ResolutionContext<'a> {
pub manifest: &'a Manifest,
pub cache: &'a Cache,
pub source_manager: &'a SourceManager,
pub operation_context: Option<&'a Arc<OperationContext>>,
}
pub struct TransitiveContext<'a> {
pub base: ResolutionContext<'a>,
pub dependency_map: &'a Arc<DashMap<DependencyKey, Vec<String>>>,
pub transitive_custom_names: &'a Arc<DashMap<DependencyKey, String>>,
pub conflict_detector: &'a mut ConflictDetector,
pub manifest_overrides: &'a ManifestOverrideIndex,
}
pub struct PatternContext<'a> {
pub base: ResolutionContext<'a>,
pub pattern_alias_map: &'a Arc<DashMap<(ResourceType, String), String>>,
}
#[derive(Debug, Clone)]
pub struct ManifestOverride {
pub filename: Option<String>,
pub target: Option<String>,
pub install: Option<bool>,
pub manifest_alias: Option<String>,
pub template_vars: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct OverrideKey {
pub resource_type: ResourceType,
pub normalized_path: String,
pub source: Option<String>,
pub tool: String,
pub variant_hash: String,
}
pub type ManifestOverrideIndex = HashMap<OverrideKey, ManifestOverride>;
#[derive(Debug, Clone)]
pub struct ResolvedDependencyInfo {
pub version_constraint: String,
pub resolved_sha: String,
pub parent_version: Option<String>,
pub parent_sha: Option<String>,
pub resolution_mode: ResolutionMode,
}
pub type ConflictDetectionKey = (crate::lockfile::ResourceId, String, String);
pub type ResolvedDependenciesMap = Arc<DashMap<ConflictDetectionKey, ResolvedDependencyInfo>>;
pub fn apply_manifest_override(
dep: &mut ResourceDependency,
override_info: &ManifestOverride,
normalized_path: &str,
) {
tracing::debug!(
"Applying manifest override to transitive dependency: {} (normalized: {})",
dep.get_path(),
normalized_path
);
if let ResourceDependency::Detailed(detailed) = dep {
let path = detailed.path.clone();
if let Some(filename) = &override_info.filename {
detailed.filename = Some(filename.clone());
}
if let Some(target) = &override_info.target {
detailed.target = Some(target.clone());
}
if let Some(install) = override_info.install {
detailed.install = Some(install);
}
if let Some(template_vars) = &override_info.template_vars {
detailed.template_vars = Some(template_vars.clone());
}
tracing::debug!(
"Applied manifest overrides to '{}': filename={:?}, target={:?}, install={:?}, template_vars={}",
path,
detailed.filename,
detailed.target,
detailed.install,
detailed.template_vars.is_some()
);
} else {
tracing::warn!(
"Cannot apply manifest override to non-detailed dependency: {}",
dep.get_path()
);
}
}
pub fn build_resource_id(dep: &ResourceDependency) -> String {
let source = dep.get_source().unwrap_or("unknown");
let path = dep.get_path();
format!("{source}:{path}")
}
pub fn normalize_lookup_path(path: &str) -> String {
use std::path::{Component, Path};
let path_obj = Path::new(path);
let mut components = Vec::new();
for component in path_obj.components() {
match component {
Component::CurDir => continue, Component::Normal(os_str) => {
components.push(os_str.to_string_lossy().to_string());
}
_ => {}
}
}
if let Some(last) = components.last_mut() {
if let Some(stem) = Path::new(last.as_str()).file_stem() {
*last = stem.to_string_lossy().to_string();
}
}
if components.is_empty() {
path.to_string()
} else {
components.join("/")
}
}
pub fn extract_filename_from_path(path: &str) -> Option<String> {
std::path::Path::new(path).file_name().and_then(|n| n.to_str()).map(String::from)
}
pub fn strip_resource_type_directory(path: &str) -> Option<String> {
use std::path::{Component, Path};
let resource_type_dirs = ["agents", "snippets", "commands", "scripts", "hooks", "mcp-servers"];
let components: Vec<_> = Path::new(path)
.components()
.filter_map(|c| match c {
Component::Normal(s) => s.to_str(),
_ => None,
})
.collect();
if components.len() > 1 {
if let Some(idx) = components.iter().position(|c| resource_type_dirs.contains(c)) {
if idx + 1 < components.len() {
return Some(components[idx + 1..].join("/"));
}
}
}
None
}
pub fn format_dependency_with_version(
resource_type: ResourceType,
name: &str,
version: &str,
) -> String {
LockfileDependencyRef::local(resource_type, name.to_string(), Some(version.to_string()))
.to_string()
}
pub fn format_dependency_without_version(resource_type: ResourceType, name: &str) -> String {
LockfileDependencyRef::local(resource_type, name.to_string(), None).to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::manifest::DetailedDependency;
#[test]
fn test_normalize_lookup_path() {
assert_eq!(normalize_lookup_path("./agents/helper.md"), "agents/helper");
assert_eq!(normalize_lookup_path("agents/helper.md"), "agents/helper");
assert_eq!(normalize_lookup_path("snippets/helpers/foo.md"), "snippets/helpers/foo");
assert_eq!(normalize_lookup_path("./foo.md"), "foo");
assert_eq!(normalize_lookup_path("./foo"), "foo");
assert_eq!(normalize_lookup_path("foo"), "foo");
}
#[test]
fn test_extract_filename_from_path() {
assert_eq!(extract_filename_from_path("agents/helper.md"), Some("helper.md".to_string()));
assert_eq!(extract_filename_from_path("foo/bar/baz.txt"), Some("baz.txt".to_string()));
assert_eq!(extract_filename_from_path("single.md"), Some("single.md".to_string()));
assert_eq!(extract_filename_from_path(""), None);
assert_eq!(extract_filename_from_path("trailing/"), Some("trailing".to_string()));
}
#[test]
fn test_strip_resource_type_directory() {
assert_eq!(
strip_resource_type_directory("agents/helpers/foo.md"),
Some("helpers/foo.md".to_string())
);
assert_eq!(
strip_resource_type_directory("snippets/rust/best-practices.md"),
Some("rust/best-practices.md".to_string())
);
assert_eq!(
strip_resource_type_directory("commands/deploy.md"),
Some("deploy.md".to_string())
);
assert_eq!(strip_resource_type_directory("foo/bar.md"), None);
assert_eq!(strip_resource_type_directory("agents"), None);
assert_eq!(
strip_resource_type_directory("mcp-servers/filesystem.json"),
Some("filesystem.json".to_string())
);
}
#[test]
fn test_format_dependency_with_version() {
assert_eq!(
format_dependency_with_version(ResourceType::Agent, "helper", "v1.0.0"),
"agent:helper@v1.0.0"
);
assert_eq!(
format_dependency_with_version(ResourceType::Snippet, "utils", "abc123"),
"snippet:utils@abc123"
);
}
#[test]
fn test_format_dependency_without_version() {
assert_eq!(
format_dependency_without_version(ResourceType::Agent, "helper"),
"agent:helper"
);
assert_eq!(
format_dependency_without_version(ResourceType::Command, "deploy"),
"command:deploy"
);
}
#[test]
fn test_build_resource_id() {
let dep = ResourceDependency::Detailed(Box::new(DetailedDependency {
source: Some("test-source".to_string()),
path: "agents/helper.md".to_string(),
version: Some("v1.0.0".to_string()),
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 resource_id = build_resource_id(&dep);
assert!(resource_id.contains("agents/helper.md"));
}
}