agpm_cli/manifest/
mod.rs

1//! Manifest file parsing and validation for AGPM projects.
2//!
3//! This module handles the `agpm.toml` manifest file that defines project
4//! dependencies and configuration. The manifest uses TOML format and follows
5//! a structure similar to Cargo.toml, providing a lockfile-based dependency
6//! management system for Claude Code resources.
7//!
8//! # Overview
9//!
10//! The manifest system enables:
11//! - Declarative dependency management through `agpm.toml`
12//! - Reproducible installations via lockfile generation
13//! - Support for multiple Git-based source repositories
14//! - Local and remote dependency resolution
15//! - Version constraint specification and validation
16//! - Transitive dependency resolution from resource metadata
17//! - Cross-platform path handling and installation
18//! - MCP (Model Context Protocol) server configuration management
19//! - Atomic file operations for reliability
20//!
21//! # Complete TOML Format Specification
22//!
23//! ## Basic Structure
24//!
25//! A `agpm.toml` manifest file consists of four main sections:
26//!
27//! ```toml
28//! # Named source repositories (optional)
29//! [sources]
30//! # Git repository URLs mapped to convenient names
31//! official = "https://github.com/example-org/agpm-official.git"
32//! community = "https://github.com/community/agpm-resources.git"
33//! private = "git@github.com:company/private-resources.git"
34//!
35//! # Installation target directories (optional)
36//! [target]
37//! # Where agents should be installed (default: ".claude/agents")
38//! agents = ".claude/agents"
39//! # Where snippets should be installed (default: ".agpm/snippets")
40//! snippets = ".agpm/snippets"
41//! # Where commands should be installed (default: ".claude/commands")
42//! commands = ".claude/commands"
43//!
44//! # Agent dependencies (optional)
45//! [agents]
46//! # Various dependency specification formats
47//! simple-agent = "../local/agents/helper.md"                    # Local path
48//! remote-agent = { source = "official", path = "agents/reviewer.md", version = "v1.0.0" }
49//! latest-agent = { source = "community", path = "agents/utils.md", version = "latest" }
50//! branch-agent = { source = "private", path = "agents/internal.md", git = "develop" }
51//! commit-agent = { source = "official", path = "agents/stable.md", git = "abc123..." }
52//! # Custom target installation directory (relative to .claude)
53//! custom-agent = { source = "official", path = "agents/special.md", version = "v1.0.0", target = "integrations/ai" }
54//!
55//! # Snippet dependencies (optional)
56//! [snippets]
57//! # Same formats as agents
58//! local-snippet = "../shared/snippets/common.md"
59//! remote-snippet = { source = "community", path = "snippets/utils.md", version = "v2.1.0" }
60//! # Custom target for special snippets
61//! integration-snippet = { source = "community", path = "snippets/api.md", version = "v1.0.0", target = "tools/snippets" }
62//!
63//! # Command dependencies (optional)
64//! [commands]
65//! # Same formats as agents and snippets
66//! local-command = "../shared/commands/helper.md"
67//! remote-command = { source = "community", path = "commands/build.md", version = "v1.0.0" }
68//! ```
69//!
70//! ## Sources Section
71//!
72//! The `[sources]` section maps convenient names to Git repository URLs:
73//!
74//! ```toml
75//! [sources]
76//! # HTTPS URLs (recommended for public repositories)
77//! official = "https://github.com/owner/agpm-resources.git"
78//! community = "https://gitlab.com/group/agpm-community.git"
79//!
80//! # SSH URLs (for private repositories with key authentication)
81//! private = "git@github.com:company/private-resources.git"
82//! internal = "git@gitlab.company.com:team/internal-resources.git"
83//!
84//! # Local Git repository URLs
85//! local-repo = "file:///absolute/path/to/local/repo"
86//!
87//! # Environment variable expansion (useful for CI/CD)
88//! dynamic = "https://github.com/${GITHUB_ORG}/resources.git"
89//! home-repo = "file://${HOME}/git/resources"
90//! ```
91//!
92//! ## Target Section
93//!
94//! The `[target]` section configures where resources are installed:
95//!
96//! ```toml
97//! [target]
98//! # Default values shown - these can be customized
99//! agents = ".claude/agents"      # Where agent .md files are copied
100//! snippets = ".agpm/snippets"  # Where snippet .md files are copied
101//! commands = ".claude/commands"  # Where command .md files are copied
102//!
103//! # Alternative configurations
104//! agents = "resources/agents"
105//! snippets = "resources/snippets"
106//! commands = "resources/commands"
107//!
108//! # Absolute paths are supported
109//! agents = "/opt/claude/agents"
110//! snippets = "/opt/claude/snippets"
111//! commands = "/opt/claude/commands"
112//! ```
113//!
114//! ## Dependency Sections
115//!
116//! Both `[agents]` and `[snippets]` sections support multiple dependency formats:
117//!
118//! ### 1. Local Path Dependencies
119//!
120//! For resources in your local filesystem:
121//!
122//! ```toml
123//! [agents]
124//! # Relative paths from manifest directory
125//! local-helper = "../shared/agents/helper.md"
126//! nearby-agent = "./local-agents/custom.md"
127//!
128//! # Absolute paths (not recommended for portability)
129//! system-agent = "/usr/local/share/claude/agents/system.md"
130//! ```
131//!
132//! Local dependencies:
133//! - Do not support version constraints
134//! - Are copied directly from the filesystem
135//! - Are not cached or managed through Git
136//! - Must exist at install time
137//!
138//! ### 2. Remote Source Dependencies
139//!
140//! For resources from Git repositories:
141//!
142//! ```toml
143//! [agents]
144//! # Basic remote dependency with semantic version
145//! code-reviewer = { source = "official", path = "agents/reviewer.md", version = "v1.0.0" }
146//!
147//! # Using latest version (not recommended for production)
148//! utils = { source = "community", path = "agents/utils.md", version = "latest" }
149//!
150//! # Specific Git branch
151//! bleeding-edge = { source = "official", path = "agents/experimental.md", git = "develop" }
152//!
153//! # Specific Git commit (maximum reproducibility)
154//! stable = { source = "official", path = "agents/stable.md", git = "a1b2c3d4e5f6..." }
155//!
156//! # Git tag (alternative to version field)
157//! tagged = { source = "community", path = "agents/tagged.md", git = "release-2.0" }
158//! ```
159//!
160//! ### 3. Custom Target Installation
161//!
162//! Dependencies can specify a custom installation directory using the `target` field:
163//!
164//! ```toml
165//! [agents]
166//! # Install to .claude/integrations/ai/ instead of .claude/agents/
167//! integration-agent = {
168//!     source = "official",
169//!     path = "agents/integration.md",
170//!     version = "v1.0.0",
171//!     target = "integrations/ai"
172//! }
173//!
174//! # Organize tools in a custom structure
175//! debug-tool = {
176//!     source = "community",
177//!     path = "agents/debugger.md",
178//!     version = "v2.0.0",
179//!     target = "development/tools"
180//! }
181//!
182//! [snippets]
183//! # Custom location for API snippets
184//! api-helper = {
185//!     source = "community",
186//!     path = "snippets/api.md",
187//!     version = "v1.0.0",
188//!     target = "api/snippets"
189//! }
190//! ```
191//!
192//! Custom targets:
193//! - Are always relative to the `.claude` directory
194//! - Leading `.claude/` or `/` are automatically stripped
195//! - Directories are created if they don't exist
196//! - Help organize resources in complex projects
197//!
198//! ### 4. Custom Filenames
199//!
200//! Dependencies can specify a custom filename using the `filename` field:
201//!
202//! ```toml
203//! [agents]
204//! # Install as "ai-assistant.md" instead of "my-ai.md"
205//! my-ai = {
206//!     source = "official",
207//!     path = "agents/complex-long-name-v2.md",
208//!     version = "v1.0.0",
209//!     filename = "ai-assistant.md"
210//! }
211//!
212//! # Change both filename and extension
213//! doc-helper = {
214//!     source = "community",
215//!     path = "agents/documentation.md",
216//!     version = "v2.0.0",
217//!     filename = "docs.txt"
218//! }
219//!
220//! # Combine custom target and filename
221//! special-tool = {
222//!     source = "official",
223//!     path = "agents/debug-analyzer-enhanced.md",
224//!     version = "v1.0.0",
225//!     target = "tools/debugging",
226//!     filename = "analyzer.markdown"
227//! }
228//!
229//! [scripts]
230//! # Rename script during installation
231//! data-processor = {
232//!     source = "community",
233//!     path = "scripts/data-processor-v3.py",
234//!     version = "v1.0.0",
235//!     filename = "process.py"
236//! }
237//! ```
238//!
239//! Custom filenames:
240//! - Include the full filename with extension
241//! - Override the default name (based on dependency key)
242//! - Work with any resource type
243//! - Can be combined with custom targets
244//!
245//! ## Version Constraint Syntax
246//!
247//! AGPM supports flexible version constraints:
248//!
249//! - `"v1.0.0"` - Exact semantic version
250//! - `"1.0.0"` - Exact version (v prefix optional)
251//! - `"latest"` - Always use the latest available version
252//! - `"main"` - Use the main/master branch HEAD
253//! - `"develop"` - Use a specific branch
254//! - `"a1b2c3d4..."` - Use a specific commit SHA
255//! - `"release-1.0"` - Use a specific Git tag
256//!
257//! ## Complete Examples
258//!
259//! ### Minimal Manifest
260//!
261//! ```toml
262//! [agents]
263//! helper = "../agents/helper.md"
264//! ```
265//!
266//! ### Production Manifest
267//!
268//! ```toml
269//! [sources]
270//! official = "https://github.com/claude-org/official-resources.git"
271//! community = "https://github.com/claude-community/resources.git"
272//! company = "git@github.com:mycompany/claude-resources.git"
273//!
274//! [target]
275//! agents = "resources/agents"
276//! snippets = "resources/snippets"
277//!
278//! [agents]
279//! # Production agents with pinned versions
280//! code-reviewer = { source = "official", path = "agents/code-reviewer.md", version = "v2.1.0" }
281//! documentation = { source = "community", path = "agents/doc-writer.md", version = "v1.5.2" }
282//! internal-helper = { source = "company", path = "agents/helper.md", version = "v1.0.0" }
283//!
284//! # Local customizations
285//! custom-agent = "./local/agents/custom.md"
286//!
287//! [snippets]
288//! # Utility snippets
289//! common-patterns = { source = "community", path = "snippets/patterns.md", version = "v1.2.0" }
290//! company-templates = { source = "company", path = "snippets/templates.md", version = "latest" }
291//! ```
292//!
293//! ## Security Considerations
294//!
295//! **CRITICAL**: Never include authentication credentials in `agpm.toml`:
296//!
297//! ```toml
298//! # ❌ NEVER DO THIS - credentials will be committed to git
299//! [sources]
300//! private = "https://token:ghp_xxxx@github.com/company/repo.git"
301//!
302//! # ✅ Instead, use global configuration in ~/.agpm/config.toml
303//! # Or use SSH keys with git@ URLs
304//! [sources]
305//! private = "git@github.com:company/repo.git"
306//! ```
307//!
308//! Authentication should be configured globally in `~/.agpm/config.toml` or
309//! through SSH keys for `git@` URLs. See [`crate::config`] for details.
310//!
311//! ## Relationship to Lockfile
312//!
313//! The manifest works together with the lockfile (`agpm.lock`):
314//!
315//! - **Manifest (`agpm.toml`)**: Declares dependencies and constraints
316//! - **Lockfile (`agpm.lock`)**: Records exact resolved versions and checksums
317//!
318//! When you run `agpm install`:
319//! 1. Reads dependencies from `agpm.toml`
320//! 2. Resolves versions within constraints  
321//! 3. Generates/updates `agpm.lock` with exact commits
322//! 4. Installs resources to target directories
323//!
324//! See [`crate::lockfile`] for lockfile format details.
325//!
326//! ## Cross-Platform Compatibility
327//!
328//! AGPM handles platform differences automatically:
329//! - Path separators (/ vs \\) are normalized
330//! - Home directory expansion (~) is supported
331//! - Environment variable expansion is available
332//! - Git commands work on Windows, macOS, and Linux
333//! - Long path support on Windows (>260 characters)
334//! - Unicode filenames and paths are fully supported
335//!
336//! ## Best Practices
337//!
338//! 1. **Use semantic versions**: Prefer `v1.0.0` over `latest`
339//! 2. **Pin production dependencies**: Use exact versions in production
340//! 3. **Organize sources logically**: Group by organization or purpose
341//! 4. **Document dependencies**: Add comments explaining why each is needed
342//! 5. **Keep manifests simple**: Avoid overly complex dependency trees
343//! 6. **Use SSH for private repos**: More secure than HTTPS tokens
344//! 7. **Test across platforms**: Verify paths work on all target systems
345//! 8. **Version control manifests**: Always commit `agpm.toml` to git
346//! 9. **Validate regularly**: Run `agpm validate` before commits
347//! 10. **Use lockfiles**: Commit `agpm.lock` for reproducible builds
348//!
349//! ## Transitive Dependencies
350//!
351//! Resources can declare their own dependencies within their files using structured
352//! metadata. This enables automatic dependency resolution without manual manifest updates.
353//!
354//! ### Supported Formats
355//!
356//! #### Markdown Files (YAML Frontmatter)
357//!
358//! ```markdown
359//! ---
360//! dependencies:
361//!   agents:
362//!     - path: agents/helper.md
363//!       version: v1.0.0
364//!     - path: agents/reviewer.md
365//!   snippets:
366//!     - path: snippets/utils.md
367//! ---
368//!
369//! # My Command Documentation
370//! ...
371//! ```
372//!
373//! #### JSON Files (Top-Level Field)
374//!
375//! ```json
376//! {
377//!   "events": ["UserPromptSubmit"],
378//!   "type": "command",
379//!   "command": ".claude/scripts/test.js",
380//!   "dependencies": {
381//!     "scripts": [
382//!       { "path": "scripts/test-runner.sh", "version": "v1.0.0" },
383//!       { "path": "scripts/validator.py" }
384//!     ],
385//!     "agents": [
386//!       { "path": "agents/code-analyzer.md", "version": "~1.2.0" }
387//!     ]
388//!   }
389//! }
390//! ```
391//!
392//! ### Key Features
393//!
394//! - **Automatic Discovery**: Dependencies extracted during resolution
395//! - **Version Inheritance**: If no version specified, parent's version is used
396//! - **Same-Source Model**: Transitive deps inherit parent's source repository
397//! - **Cycle Detection**: Circular dependency loops are detected and prevented
398//! - **Topological Ordering**: Dependencies installed in correct order
399//! - **Optional Resolution**: Can be disabled with `--no-transitive` flag
400//!
401//! ### Data Structures
402//!
403//! Transitive dependencies are represented by:
404//! - [`DependencySpec`]: Individual dependency specification (path + optional version)
405//! - [`DependencyMetadata`]: Collection of dependencies by resource type
406//! - [`DetailedDependency::dependencies`]: Field storing extracted transitive deps
407//!
408//! ### Processing Flow
409//!
410//! 1. Manifest dependencies are resolved first
411//! 2. Resource files are checked for metadata (YAML frontmatter or JSON fields)
412//! 3. Discovered dependencies are added to dependency graph
413//! 4. Graph is validated for cycles
414//! 5. Dependencies are resolved in topological order
415//! 6. All resources (direct + transitive) are installed
416//!
417//! See [`dependency_spec`] module for detailed specification formats.
418//!
419//! ## Error Handling
420//!
421//! The manifest module provides comprehensive error handling with:
422//! - **Context-rich errors**: Detailed messages with actionable suggestions
423//! - **Validation errors**: Clear explanations of manifest problems
424//! - **I/O errors**: Helpful context for file system issues
425//! - **TOML parsing errors**: Specific syntax error locations
426//! - **Security validation**: Detection of potential security issues
427//!
428//! All errors implement [`std::error::Error`] and provide both user-friendly
429//! messages and programmatic access to error details.
430//!
431//! ## Performance Characteristics
432//!
433//! - **Parsing**: O(n) where n is the manifest file size
434//! - **Validation**: O(d) where d is the number of dependencies
435//! - **Serialization**: O(n) where n is the total data size
436//! - **Memory usage**: Proportional to manifest complexity
437//! - **Thread safety**: All operations are thread-safe
438//!
439//! ## Integration with Other Modules
440//!
441//! The manifest module works closely with other AGPM modules:
442//!
443//! ### With [`crate::resolver`]
444//!
445//! ```rust,ignore
446//! use agpm_cli::manifest::Manifest;
447//! use agpm_cli::resolver::DependencyResolver;
448//!
449//! let manifest = Manifest::load(&project_path.join("agpm.toml"))?;
450//! let resolver = DependencyResolver::new(&manifest);
451//! let resolved = resolver.resolve_all().await?;
452//! ```
453//!
454//! ### With [`crate::lockfile`]
455//!
456//! ```rust,ignore  
457//! use agpm_cli::manifest::Manifest;
458//! use agpm_cli::lockfile::LockFile;
459//!
460//! let manifest = Manifest::load(&project_path.join("agpm.toml"))?;
461//! let lockfile = LockFile::generate_from_manifest(&manifest).await?;
462//! lockfile.save(&project_path.join("agpm.lock"))?;
463//! ```
464//!
465//! ### With [`crate::git`] for Source Management
466//!
467//! ```rust,ignore
468//! use agpm_cli::manifest::Manifest;
469//! use agpm_cli::git::GitManager;
470//!
471//! let manifest = Manifest::load(&project_path.join("agpm.toml"))?;
472//! let git = GitManager::new(&cache_dir);
473//!
474//! for (name, url) in &manifest.sources {
475//!     git.clone_or_update(name, url).await?;
476//! }
477//! ```
478
479pub mod dependency_spec;
480pub mod helpers;
481pub mod patches;
482pub mod resource_dependency;
483pub mod tool_config;
484
485#[cfg(test)]
486mod manifest_tests;
487#[cfg(test)]
488mod tool_config_tests;
489
490use anyhow::{Context, Result};
491use serde::{Deserialize, Serialize};
492use std::collections::{BTreeMap, HashMap};
493use std::path::{Path, PathBuf};
494
495pub use dependency_spec::{DependencyMetadata, DependencySpec};
496pub use helpers::{expand_url, find_manifest, find_manifest_from, find_manifest_with_optional};
497pub use patches::{ManifestPatches, PatchConflict, PatchData, PatchOrigin};
498pub use resource_dependency::{DetailedDependency, ResourceDependency};
499pub use tool_config::{ArtifactTypeConfig, ResourceConfig, ToolsConfig, WellKnownTool};
500
501/// The main manifest file structure representing a complete `agpm.toml` file.
502///
503/// This struct encapsulates all configuration for a AGPM project, including
504/// source repositories, installation targets, and resource dependencies.
505/// It provides the foundation for declarative dependency management similar
506/// to Cargo's `Cargo.toml`.
507///
508/// # Structure
509///
510/// - **Sources**: Named Git repositories that can be referenced by dependencies
511/// - **Target**: Installation directories for different resource types
512/// - **Agents**: AI agent dependencies (`.md` files with agent definitions)
513/// - **Snippets**: Code snippet dependencies (`.md` files with reusable code)
514/// - **Commands**: Claude Code command dependencies (`.md` files with slash commands)
515///
516/// # Serialization
517///
518/// The struct uses Serde for TOML serialization/deserialization with these behaviors:
519/// - Empty collections are omitted from serialized output for cleaner files
520/// - Default values are automatically applied for missing fields
521/// - Field names match TOML section names exactly
522///
523/// # Thread Safety
524///
525/// This struct is thread-safe and can be shared across async tasks safely.
526///
527/// # Examples
528///
529/// ```rust,no_run
530/// use agpm_cli::manifest::{Manifest, ResourceDependency};
531///
532/// // Create a new empty manifest
533/// let mut manifest = Manifest::new();
534///
535/// // Add a source repository
536/// manifest.add_source(
537///     "community".to_string(),
538///     "https://github.com/claude-community/resources.git".to_string()
539/// );
540///
541/// // Add a dependency
542/// manifest.add_dependency(
543///     "helper".to_string(),
544///     ResourceDependency::Simple("../local/helper.md".to_string()),
545///     true  // is_agent = true
546/// );
547/// ```
548/// Project-specific template variables for AI coding assistants.
549///
550/// An arbitrary map of user-defined variables that can be referenced in resource templates.
551/// This provides maximum flexibility for teams to organize project context however they want,
552/// without imposing any predefined structure.
553///
554/// # Use Case: AI Agent Context
555///
556/// When AI agents work on your codebase, they need context about:
557/// - Where to find coding standards and style guides
558/// - What conventions to follow (formatting, naming, patterns)
559/// - Where architecture and design docs are located
560/// - Project-specific requirements (testing, security, performance)
561///
562/// # Template Access
563///
564/// All variables are accessible in templates under the `agpm.project` namespace.
565/// The structure is completely user-defined.
566///
567/// # Examples
568///
569/// ## Flexible Structure - Organize However You Want
570/// ```toml
571/// [project]
572/// # Top-level variables
573/// style_guide = "docs/STYLE_GUIDE.md"
574/// max_line_length = 100
575/// test_framework = "pytest"
576///
577/// # Nested sections (optional, just for organization)
578/// [project.paths]
579/// architecture = "docs/ARCHITECTURE.md"
580/// conventions = "docs/CONVENTIONS.md"
581///
582/// [project.standards]
583/// indent_style = "spaces"
584/// indent_size = 4
585/// ```
586///
587/// ## Template Usage
588/// ```markdown
589/// # Code Reviewer
590/// Follow guidelines at: {{ agpm.project.style_guide }}
591/// Max line length: {{ agpm.project.max_line_length }}
592/// Architecture: {{ agpm.project.paths.architecture }}
593/// ```
594///
595/// ## Any Structure Works
596/// ```toml
597/// [project]
598/// whatever = "you want"
599/// numbers = 42
600/// arrays = ["work", "too"]
601///
602/// [project.deeply.nested.structure]
603/// is_allowed = true
604/// ```
605#[derive(Debug, Clone, Serialize, Deserialize, Default)]
606pub struct ProjectConfig(toml::map::Map<String, toml::Value>);
607
608impl ProjectConfig {
609    /// Convert this ProjectConfig to a serde_json::Value for template rendering.
610    ///
611    /// This method handles conversion of TOML values to JSON values, which is necessary
612    /// for proper Tera template rendering.
613    ///
614    /// # Examples
615    ///
616    /// ```rust,no_run
617    /// use agpm_cli::manifest::ProjectConfig;
618    ///
619    /// let mut config_map = toml::map::Map::new();
620    /// config_map.insert("style_guide".to_string(), toml::Value::String("docs/STYLE.md".into()));
621    /// let config = ProjectConfig::from(config_map);
622    ///
623    /// let json = config.to_json_value();
624    /// // Use json in Tera template context
625    /// ```
626    pub fn to_json_value(&self) -> serde_json::Value {
627        toml_value_to_json(&toml::Value::Table(self.0.clone()))
628    }
629}
630
631impl From<toml::map::Map<String, toml::Value>> for ProjectConfig {
632    fn from(map: toml::map::Map<String, toml::Value>) -> Self {
633        Self(map)
634    }
635}
636
637/// Convert a toml::Value to serde_json::Value.
638pub(crate) fn toml_value_to_json(value: &toml::Value) -> serde_json::Value {
639    match value {
640        toml::Value::String(s) => serde_json::Value::String(s.clone()),
641        toml::Value::Integer(i) => serde_json::Value::Number((*i).into()),
642        toml::Value::Float(f) => serde_json::Number::from_f64(*f)
643            .map(serde_json::Value::Number)
644            .unwrap_or(serde_json::Value::Null),
645        toml::Value::Boolean(b) => serde_json::Value::Bool(*b),
646        toml::Value::Datetime(dt) => serde_json::Value::String(dt.to_string()),
647        toml::Value::Array(arr) => {
648            serde_json::Value::Array(arr.iter().map(toml_value_to_json).collect())
649        }
650        toml::Value::Table(table) => {
651            // Sort keys to ensure deterministic JSON serialization
652            let mut keys: Vec<_> = table.keys().collect();
653            keys.sort();
654            let map: serde_json::Map<String, serde_json::Value> =
655                keys.into_iter().map(|k| (k.clone(), toml_value_to_json(&table[k]))).collect();
656            serde_json::Value::Object(map)
657        }
658    }
659}
660
661/// Convert JSON value to TOML value for template variable merging.
662///
663/// Handles JSON null as empty string since TOML lacks a null type.
664/// Used when merging template_vars (JSON) with project config (TOML).
665#[cfg(test)]
666pub(crate) fn json_value_to_toml(value: &serde_json::Value) -> toml::Value {
667    match value {
668        serde_json::Value::String(s) => toml::Value::String(s.clone()),
669        serde_json::Value::Number(n) => {
670            if let Some(i) = n.as_i64() {
671                toml::Value::Integer(i)
672            } else if let Some(f) = n.as_f64() {
673                toml::Value::Float(f)
674            } else {
675                // Fallback for numbers that don't fit i64 or f64
676                toml::Value::String(n.to_string())
677            }
678        }
679        serde_json::Value::Bool(b) => toml::Value::Boolean(*b),
680        serde_json::Value::Null => {
681            // TOML doesn't have a null type - represent as empty string
682            toml::Value::String(String::new())
683        }
684        serde_json::Value::Array(arr) => {
685            toml::Value::Array(arr.iter().map(json_value_to_toml).collect())
686        }
687        serde_json::Value::Object(obj) => {
688            let table: toml::map::Map<String, toml::Value> =
689                obj.iter().map(|(k, v)| (k.clone(), json_value_to_toml(v))).collect();
690            toml::Value::Table(table)
691        }
692    }
693}
694
695#[derive(Debug, Clone, Serialize, Deserialize)]
696pub struct Manifest {
697    /// Named source repositories mapped to their Git URLs.
698    ///
699    /// Keys are short, convenient names used in dependency specifications.
700    /// Values are Git repository URLs (HTTPS, SSH, or local file:// URLs).
701    ///
702    /// **Security Note**: Never include authentication tokens in these URLs.
703    /// Use SSH keys or configure authentication in the global config file.
704    ///
705    /// # Examples
706    ///
707    /// ```toml
708    /// [sources]
709    /// official = "https://github.com/claude-org/official.git"
710    /// private = "git@github.com:company/private.git"
711    /// local = "file:///home/user/local-repo"
712    /// ```
713    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
714    pub sources: HashMap<String, String>,
715
716    /// Tool type configurations for multi-tool support.
717    ///
718    /// Maps tool type names (claude-code, opencode, agpm, custom) to their
719    /// installation configurations. This replaces the old `target` field and
720    /// enables support for multiple tools and custom tool types.
721    ///
722    /// See [`ToolsConfig`] for details on configuration format.
723    #[serde(rename = "tools", skip_serializing_if = "Option::is_none")]
724    pub tools: Option<ToolsConfig>,
725
726    /// Agent dependencies mapping names to their specifications.
727    ///
728    /// Agents are typically AI model definitions, prompts, or behavioral
729    /// specifications stored as Markdown files. Each dependency can be
730    /// either local (filesystem path) or remote (from a Git source).
731    ///
732    /// See [`ResourceDependency`] for specification format details.
733    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
734    pub agents: HashMap<String, ResourceDependency>,
735
736    /// Snippet dependencies mapping names to their specifications.
737    ///
738    /// Snippets are typically reusable code templates, examples, or
739    /// documentation stored as Markdown files. They follow the same
740    /// dependency format as agents.
741    ///
742    /// See [`ResourceDependency`] for specification format details.
743    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
744    pub snippets: HashMap<String, ResourceDependency>,
745
746    /// Command dependencies mapping names to their specifications.
747    ///
748    /// Commands are Claude Code slash commands that provide custom functionality
749    /// and automation within the Claude Code interface. They follow the same
750    /// dependency format as agents and snippets.
751    ///
752    /// See [`ResourceDependency`] for specification format details.
753    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
754    pub commands: HashMap<String, ResourceDependency>,
755
756    /// MCP server configurations mapping names to their specifications.
757    ///
758    /// MCP servers provide integrations with external systems and services,
759    /// allowing Claude Code to connect to databases, APIs, and other tools.
760    /// MCP servers are JSON configuration files that get installed to
761    /// `.mcp.json` (no separate directory - configurations are merged into the JSON file).
762    ///
763    /// See [`ResourceDependency`] for specification format details.
764    #[serde(default, skip_serializing_if = "HashMap::is_empty", rename = "mcp-servers")]
765    pub mcp_servers: HashMap<String, ResourceDependency>,
766
767    /// Script dependencies mapping names to their specifications.
768    ///
769    /// Scripts are executable files (.sh, .js, .py, etc.) that can be run by hooks
770    /// or independently. They are installed to `.claude/scripts/` and can be
771    /// referenced by hook configurations.
772    ///
773    /// See [`ResourceDependency`] for specification format details.
774    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
775    pub scripts: HashMap<String, ResourceDependency>,
776
777    /// Hook dependencies mapping names to their specifications.
778    ///
779    /// Hooks are JSON configuration files that define event-based automation
780    /// in Claude Code. They specify when to run scripts based on tool usage,
781    /// prompts, and other events. Hook configurations are merged into
782    /// `settings.local.json`.
783    ///
784    /// See [`ResourceDependency`] for specification format details.
785    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
786    pub hooks: HashMap<String, ResourceDependency>,
787
788    /// Patches for overriding resource metadata.
789    ///
790    /// Patches allow overriding YAML frontmatter fields (like `model`) in
791    /// resources without forking upstream repositories. They are keyed by
792    /// resource type and manifest alias.
793    ///
794    /// # Examples
795    ///
796    /// ```toml
797    /// [patch.agents.my-agent]
798    /// model = "claude-3-haiku"
799    /// temperature = "0.7"
800    /// ```
801    #[serde(default, skip_serializing_if = "ManifestPatches::is_empty", rename = "patch")]
802    pub patches: ManifestPatches,
803
804    /// Project-level patches (from agpm.toml).
805    ///
806    /// This field is not serialized - it's populated during loading to track
807    /// which patches came from the project manifest vs private config.
808    #[serde(skip)]
809    pub project_patches: ManifestPatches,
810
811    /// Private patches (from agpm.private.toml).
812    ///
813    /// This field is not serialized - it's populated during loading to track
814    /// which patches came from private config. These are kept separate from
815    /// project patches to maintain deterministic lockfiles.
816    #[serde(skip)]
817    pub private_patches: ManifestPatches,
818
819    /// Default tool overrides for resource types.
820    ///
821    /// Allows users to override which tool is used by default when a dependency
822    /// doesn't explicitly specify a tool. Keys are resource type names (agents,
823    /// snippets, commands, scripts, hooks, mcp-servers), values are tool names
824    /// (claude-code, opencode, agpm, or custom tool names).
825    ///
826    /// # Examples
827    ///
828    /// ```toml
829    /// [default-tools]
830    /// snippets = "claude-code"  # Override default for Claude-only users
831    /// agents = "claude-code"    # Explicit (already the default)
832    /// commands = "opencode"     # Use OpenCode by default for commands
833    /// ```
834    ///
835    /// # Built-in Defaults (when not configured)
836    ///
837    /// - `snippets` → `"agpm"` (shared infrastructure)
838    /// - All other resource types → `"claude-code"`
839    #[serde(default, skip_serializing_if = "HashMap::is_empty", rename = "default-tools")]
840    pub default_tools: HashMap<String, String>,
841
842    /// Project-specific template variables.
843    ///
844    /// Custom project configuration that can be referenced in resource templates
845    /// via Tera template syntax. This allows teams to define project-specific
846    /// values like paths, standards, and conventions that are then available
847    /// throughout all installed resources.
848    ///
849    /// Template access: `{{ agpm.project.name }}`, `{{ agpm.project.paths.style_guide }}`
850    ///
851    /// # Examples
852    ///
853    /// ```toml
854    /// [project]
855    /// name = "My Project"
856    /// version = "2.0.0"
857    ///
858    /// [project.paths]
859    /// style_guide = "docs/STYLE_GUIDE.md"
860    /// ```
861    #[serde(skip_serializing_if = "Option::is_none")]
862    pub project: Option<ProjectConfig>,
863
864    /// Directory containing the manifest file (for resolving relative paths).
865    ///
866    /// This field is populated when loading the manifest and is used to resolve
867    /// relative paths in dependencies, particularly for path-only dependencies
868    /// and their transitive dependencies.
869    ///
870    /// This field is not serialized and only exists at runtime.
871    #[serde(skip)]
872    pub manifest_dir: Option<std::path::PathBuf>,
873}
874
875/// A resource dependency specification supporting multiple formats.
876///
877/// Dependencies can be specified in two main formats to balance simplicity
878/// with flexibility. The enum uses Serde's `untagged` attribute to automatically
879/// deserialize the correct variant based on the TOML structure.
880///
881/// # Variants
882///
883/// ## Simple Dependencies
884///
885/// For local file dependencies, just specify the path directly:
886///
887/// ```toml
888/// [agents]
889/// local-helper = "../shared/agents/helper.md"
890/// nearby-agent = "./local/custom-agent.md"
891/// ```
892///
893/// ## Detailed Dependencies
894///
895/// For remote dependencies or when you need more control:
896///
897/// ```toml
898/// [agents]
899/// # Remote dependency with version
900/// code-reviewer = { source = "official", path = "agents/reviewer.md", version = "v1.0.0" }
901///
902/// # Remote dependency with git reference
903/// experimental = { source = "community", path = "agents/new.md", git = "develop" }
904///
905/// # Local dependency with explicit path (equivalent to simple form)
906/// local-tool = { path = "../tools/agent.md" }
907/// ```
908///
909/// # Validation Rules
910///
911/// - **Local dependencies** (no source): Cannot have version constraints
912/// - **Remote dependencies** (with source): Must have either `version` or `git` field
913/// - **Path field**: Required and cannot be empty
914/// - **Source field**: Must reference an existing source in the `[sources]` section
915///
916/// # Type Safety
917///
918/// The enum ensures type safety at compile time while providing runtime
919/// validation through the [`Manifest::validate`] method.
920///
921/// # Serialization Behavior
922///
923/// - Simple paths serialize directly as strings
924/// - Detailed specs serialize as TOML inline tables
925/// - Empty optional fields are omitted for cleaner output
926/// - Deserialization is automatic based on TOML structure
927///
928/// # Memory Layout
929///
930/// This enum uses `#[serde(untagged)]` for automatic variant detection,
931/// which means deserialization tries the `Detailed` variant first, then
932/// falls back to `Simple`. This is efficient for the expected usage patterns
933/// where detailed dependencies are more common in larger projects.
934impl Manifest {
935    /// Create a new empty manifest with default configuration.
936    ///
937    /// The new manifest will have:
938    /// - No sources defined
939    /// - Default target directories (`.claude/agents` and `.agpm/snippets`)
940    /// - No dependencies
941    ///
942    /// This is typically used when programmatically building a manifest or
943    /// as a starting point for adding dependencies.
944    ///
945    /// # Examples
946    ///
947    /// ```rust,no_run
948    /// use agpm_cli::manifest::Manifest;
949    ///
950    /// let manifest = Manifest::new();
951    /// assert!(manifest.sources.is_empty());
952    /// assert!(manifest.agents.is_empty());
953    /// assert!(manifest.snippets.is_empty());
954    /// assert!(manifest.commands.is_empty());
955    /// assert!(manifest.mcp_servers.is_empty());
956    /// ```
957    #[must_use]
958    #[allow(deprecated)]
959    pub fn new() -> Self {
960        Self {
961            sources: HashMap::new(),
962            tools: None,
963            agents: HashMap::new(),
964            snippets: HashMap::new(),
965            commands: HashMap::new(),
966            mcp_servers: HashMap::new(),
967            scripts: HashMap::new(),
968            hooks: HashMap::new(),
969            patches: ManifestPatches::new(),
970            project_patches: ManifestPatches::new(),
971            private_patches: ManifestPatches::new(),
972            default_tools: HashMap::new(),
973            project: None,
974            manifest_dir: None,
975        }
976    }
977
978    /// Load and parse a manifest from a TOML file.
979    ///
980    /// This method reads the specified file, parses it as TOML, deserializes
981    /// it into a [`Manifest`] struct, and validates the result. The entire
982    /// operation is atomic - either the manifest loads successfully or an
983    /// error is returned.
984    ///
985    /// # Validation
986    ///
987    /// After parsing, the manifest is automatically validated to ensure:
988    /// - All dependency sources reference valid entries in the `[sources]` section
989    /// - Required fields are present and non-empty
990    /// - Version constraints are properly specified for remote dependencies
991    /// - Source URLs use supported protocols
992    /// - No version conflicts exist between dependencies
993    ///
994    /// # Error Handling
995    ///
996    /// Returns detailed errors for common problems:
997    /// - **File I/O errors**: File not found, permission denied, etc.
998    /// - **TOML syntax errors**: Invalid TOML format with helpful suggestions
999    /// - **Validation errors**: Logical inconsistencies in the manifest
1000    /// - **Security errors**: Unsafe URL patterns or credential leakage
1001    ///
1002    /// All errors include contextual information and actionable suggestions.
1003    ///
1004    /// # Examples
1005    ///
1006    /// ```rust,no_run,ignore
1007    /// use agpm_cli::manifest::Manifest;
1008    /// use std::path::Path;
1009    ///
1010    /// // Load a manifest file
1011    /// let manifest = Manifest::load(Path::new("agpm.toml"))?;
1012    ///
1013    /// // Access parsed data
1014    /// println!("Found {} sources", manifest.sources.len());
1015    /// println!("Found {} agents", manifest.agents.len());
1016    /// println!("Found {} snippets", manifest.snippets.len());
1017    /// # Ok::<(), anyhow::Error>(())
1018    /// ```
1019    ///
1020    /// # File Format
1021    ///
1022    /// Expects a valid TOML file following the AGPM manifest format.
1023    /// See the module-level documentation for complete format specification.
1024    pub fn load(path: &Path) -> Result<Self> {
1025        let content = std::fs::read_to_string(path).with_context(|| {
1026            format!(
1027                "Cannot read manifest file: {}\n\n\
1028                    Possible causes:\n\
1029                    - File doesn't exist or has been moved\n\
1030                    - Permission denied (check file ownership)\n\
1031                    - File is locked by another process",
1032                path.display()
1033            )
1034        })?;
1035
1036        let mut manifest: Self = toml::from_str(&content)
1037            .map_err(|e| crate::core::AgpmError::ManifestParseError {
1038                file: path.display().to_string(),
1039                reason: e.to_string(),
1040            })
1041            .with_context(|| {
1042                format!(
1043                    "Invalid TOML syntax in manifest file: {}\n\n\
1044                    Common TOML syntax errors:\n\
1045                    - Missing quotes around strings\n\
1046                    - Unmatched brackets [ ] or braces {{ }}\n\
1047                    - Invalid characters in keys or values\n\
1048                    - Incorrect indentation or structure",
1049                    path.display()
1050                )
1051            })?;
1052
1053        // Apply resource-type-specific defaults for tool
1054        // Snippets default to "agpm" (shared infrastructure) instead of "claude-code"
1055        manifest.apply_tool_defaults();
1056
1057        // Store the manifest directory for resolving relative paths
1058        manifest.manifest_dir = Some(
1059            path.parent()
1060                .ok_or_else(|| anyhow::anyhow!("Manifest path has no parent directory"))?
1061                .to_path_buf(),
1062        );
1063
1064        manifest.validate()?;
1065
1066        Ok(manifest)
1067    }
1068
1069    /// Load manifest with private config merged.
1070    ///
1071    /// Loads the project manifest from `agpm.toml` and then attempts to load
1072    /// `agpm.private.toml` from the same directory. If a private config exists,
1073    /// its patches are merged with the project patches (private silently takes precedence).
1074    ///
1075    /// Any conflicts (same field defined in both files with different values) are
1076    /// returned for informational purposes only. Private patches always override
1077    /// project patches without raising an error.
1078    ///
1079    /// # Arguments
1080    ///
1081    /// * `path` - Path to the project manifest file (`agpm.toml`)
1082    ///
1083    /// # Returns
1084    ///
1085    /// A manifest with merged patches and a list of any conflicts detected (for
1086    /// informational/debugging purposes).
1087    ///
1088    /// # Examples
1089    ///
1090    /// ```no_run
1091    /// use agpm_cli::manifest::Manifest;
1092    /// use std::path::Path;
1093    ///
1094    /// let (manifest, conflicts) = Manifest::load_with_private(Path::new("agpm.toml"))?;
1095    /// // Conflicts are informational only - private patches already won
1096    /// if !conflicts.is_empty() {
1097    ///     eprintln!("Note: {} private patch(es) override project settings", conflicts.len());
1098    /// }
1099    /// # Ok::<(), anyhow::Error>(())
1100    /// ```
1101    pub fn load_with_private(path: &Path) -> Result<(Self, Vec<PatchConflict>)> {
1102        // Load the main project manifest
1103        let mut manifest = Self::load(path)?;
1104
1105        // Store project patches before merging
1106        manifest.project_patches = manifest.patches.clone();
1107
1108        // Try to load private config
1109        let private_path = if let Some(parent) = path.parent() {
1110            parent.join("agpm.private.toml")
1111        } else {
1112            PathBuf::from("agpm.private.toml")
1113        };
1114
1115        if private_path.exists() {
1116            let private_manifest = Self::load_private(&private_path)?;
1117
1118            // Store private patches
1119            manifest.private_patches = private_manifest.patches.clone();
1120
1121            // Merge patches (private takes precedence)
1122            let (merged_patches, conflicts) =
1123                manifest.patches.merge_with(&private_manifest.patches);
1124            manifest.patches = merged_patches;
1125
1126            Ok((manifest, conflicts))
1127        } else {
1128            // No private config, keep private_patches empty
1129            manifest.private_patches = ManifestPatches::new();
1130            Ok((manifest, Vec::new()))
1131        }
1132    }
1133
1134    /// Load a private manifest file.
1135    ///
1136    /// Private manifests can only contain patches - they cannot define sources,
1137    /// tools, or dependencies. This method loads and validates that the private
1138    /// config follows these rules.
1139    ///
1140    /// # Arguments
1141    ///
1142    /// * `path` - Path to the private manifest file (`agpm.private.toml`)
1143    ///
1144    /// # Errors
1145    ///
1146    /// Returns an error if:
1147    /// - The file cannot be read
1148    /// - The TOML syntax is invalid
1149    /// - The private config contains non-patch fields
1150    fn load_private(path: &Path) -> Result<Self> {
1151        let content = std::fs::read_to_string(path).with_context(|| {
1152            format!(
1153                "Cannot read private manifest file: {}\n\n\
1154                    Possible causes:\n\
1155                    - File doesn't exist or has been moved\n\
1156                    - Permission denied (check file ownership)\n\
1157                    - File is locked by another process",
1158                path.display()
1159            )
1160        })?;
1161
1162        let manifest: Self = toml::from_str(&content)
1163            .map_err(|e| crate::core::AgpmError::ManifestParseError {
1164                file: path.display().to_string(),
1165                reason: e.to_string(),
1166            })
1167            .with_context(|| {
1168                format!(
1169                    "Invalid TOML syntax in private manifest file: {}\n\n\
1170                    Common TOML syntax errors:\n\
1171                    - Missing quotes around strings\n\
1172                    - Unmatched brackets [ ] or braces {{ }}\n\
1173                    - Invalid characters in keys or values\n\
1174                    - Incorrect indentation or structure",
1175                    path.display()
1176                )
1177            })?;
1178
1179        // Validate that private config only contains patches
1180        if !manifest.sources.is_empty()
1181            || manifest.tools.is_some()
1182            || !manifest.agents.is_empty()
1183            || !manifest.snippets.is_empty()
1184            || !manifest.commands.is_empty()
1185            || !manifest.mcp_servers.is_empty()
1186            || !manifest.scripts.is_empty()
1187            || !manifest.hooks.is_empty()
1188        {
1189            anyhow::bail!(
1190                "Private manifest file ({}) can only contain [patch] sections, not sources, tools, or dependencies",
1191                path.display()
1192            );
1193        }
1194
1195        Ok(manifest)
1196    }
1197
1198    /// Get the default tool for a resource type.
1199    ///
1200    /// Checks the `[default-tools]` configuration first, then falls back to
1201    /// the built-in defaults:
1202    /// - `snippets` → `"agpm"` (shared infrastructure)
1203    /// - All other resource types → `"claude-code"`
1204    ///
1205    /// # Arguments
1206    ///
1207    /// * `resource_type` - The resource type to get the default tool for
1208    ///
1209    /// # Returns
1210    ///
1211    /// The default tool name as a string.
1212    ///
1213    /// # Examples
1214    ///
1215    /// ```rust,no_run
1216    /// use agpm_cli::manifest::Manifest;
1217    /// use agpm_cli::core::ResourceType;
1218    ///
1219    /// let manifest = Manifest::new();
1220    /// assert_eq!(manifest.get_default_tool(ResourceType::Snippet), "agpm");
1221    /// assert_eq!(manifest.get_default_tool(ResourceType::Agent), "claude-code");
1222    /// ```
1223    #[must_use]
1224    pub fn get_default_tool(&self, resource_type: crate::core::ResourceType) -> String {
1225        // Get the resource name in plural form for consistency with TOML section names
1226        // (agents, snippets, commands, etc.)
1227        let resource_name = match resource_type {
1228            crate::core::ResourceType::Agent => "agents",
1229            crate::core::ResourceType::Snippet => "snippets",
1230            crate::core::ResourceType::Command => "commands",
1231            crate::core::ResourceType::Script => "scripts",
1232            crate::core::ResourceType::Hook => "hooks",
1233            crate::core::ResourceType::McpServer => "mcp-servers",
1234        };
1235
1236        // Check if there's a configured override
1237        if let Some(tool) = self.default_tools.get(resource_name) {
1238            return tool.clone();
1239        }
1240
1241        // Fall back to built-in defaults
1242        resource_type.default_tool().to_string()
1243    }
1244
1245    fn apply_tool_defaults(&mut self) {
1246        // Apply resource-type-specific defaults only when tool is not explicitly specified
1247        for resource_type in [
1248            crate::core::ResourceType::Snippet,
1249            crate::core::ResourceType::Agent,
1250            crate::core::ResourceType::Command,
1251            crate::core::ResourceType::Script,
1252            crate::core::ResourceType::Hook,
1253            crate::core::ResourceType::McpServer,
1254        ] {
1255            // Get the default tool before the mutable borrow to avoid borrow conflicts
1256            let default_tool = self.get_default_tool(resource_type);
1257
1258            if let Some(deps) = self.get_dependencies_mut(resource_type) {
1259                for dependency in deps.values_mut() {
1260                    if let ResourceDependency::Detailed(details) = dependency {
1261                        if details.tool.is_none() {
1262                            details.tool = Some(default_tool.clone());
1263                        }
1264                    }
1265                }
1266            }
1267        }
1268    }
1269
1270    /// Save the manifest to a TOML file with pretty formatting.
1271    ///
1272    /// This method serializes the manifest to TOML format and writes it to the
1273    /// specified file path. The output is pretty-printed for human readability
1274    /// and follows TOML best practices.
1275    ///
1276    /// # Formatting
1277    ///
1278    /// The generated TOML file will:
1279    /// - Use consistent indentation and spacing
1280    /// - Omit empty sections for cleaner output
1281    /// - Order sections logically (sources, target, agents, snippets)
1282    /// - Include inline tables for detailed dependencies
1283    ///
1284    /// # Atomic Operation
1285    ///
1286    /// The save operation is atomic - the file is either completely written
1287    /// or left unchanged. This prevents corruption if the operation fails
1288    /// partway through.
1289    ///
1290    /// # Error Handling
1291    ///
1292    /// Returns detailed errors for common problems:
1293    /// - **Permission denied**: Insufficient write permissions
1294    /// - **Directory doesn't exist**: Parent directory missing  
1295    /// - **Disk full**: Insufficient storage space
1296    /// - **File locked**: Another process has the file open
1297    ///
1298    /// # Examples
1299    ///
1300    /// ```rust,no_run
1301    /// use agpm_cli::manifest::Manifest;
1302    /// use std::path::Path;
1303    ///
1304    /// let mut manifest = Manifest::new();
1305    /// manifest.add_source(
1306    ///     "official".to_string(),
1307    ///     "https://github.com/claude-org/resources.git".to_string()
1308    /// );
1309    ///
1310    /// // Save to file
1311    /// # use tempfile::tempdir;
1312    /// # let temp_dir = tempdir()?;
1313    /// # let manifest_path = temp_dir.path().join("agpm.toml");
1314    /// manifest.save(&manifest_path)?;
1315    /// # Ok::<(), anyhow::Error>(())
1316    /// ```
1317    ///
1318    /// # Output Format
1319    ///
1320    /// The generated file will follow this structure:
1321    ///
1322    /// ```toml
1323    /// [sources]
1324    /// official = "https://github.com/claude-org/resources.git"
1325    ///
1326    /// [target]
1327    /// agents = ".claude/agents"
1328    /// snippets = ".agpm/snippets"
1329    ///
1330    /// [agents]
1331    /// helper = { source = "official", path = "agents/helper.md", version = "v1.0.0" }
1332    ///
1333    /// [snippets]
1334    /// utils = { source = "official", path = "snippets/utils.md", version = "v1.0.0" }
1335    /// ```
1336    pub fn save(&self, path: &Path) -> Result<()> {
1337        // Serialize to a document first so we can control formatting
1338        let mut doc = toml_edit::ser::to_document(self)
1339            .with_context(|| "Failed to serialize manifest data to TOML format")?;
1340
1341        // Convert top-level inline tables to regular tables (section headers)
1342        // This keeps [sources], [agents], etc. as sections but nested values stay inline
1343        for (_key, value) in doc.iter_mut() {
1344            if let Some(inline_table) = value.as_inline_table() {
1345                // Convert inline table to regular table
1346                let table = inline_table.clone().into_table();
1347                *value = toml_edit::Item::Table(table);
1348            }
1349        }
1350
1351        let content = doc.to_string();
1352
1353        std::fs::write(path, content).with_context(|| {
1354            format!(
1355                "Cannot write manifest file: {}\n\n\
1356                    Possible causes:\n\
1357                    - Permission denied (try running with elevated permissions)\n\
1358                    - Directory doesn't exist\n\
1359                    - Disk is full or read-only\n\
1360                    - File is locked by another process",
1361                path.display()
1362            )
1363        })?;
1364
1365        Ok(())
1366    }
1367
1368    /// Validate the manifest structure and enforce business rules.
1369    ///
1370    /// This method performs comprehensive validation of the manifest to ensure
1371    /// logical consistency, security best practices, and correct dependency
1372    /// relationships. It's automatically called during [`Self::load`] but can
1373    /// also be used independently to validate programmatically constructed manifests.
1374    ///
1375    /// # Validation Rules
1376    ///
1377    /// ## Source Validation
1378    /// - All source URLs must use supported protocols (HTTPS, SSH, git://, file://)
1379    /// - No plain directory paths allowed as sources (must use file:// URLs)
1380    /// - No authentication tokens embedded in URLs (security check)
1381    /// - Environment variable expansion is validated for syntax
1382    ///
1383    /// ## Dependency Validation  
1384    /// - All dependency paths must be non-empty
1385    /// - Remote dependencies must reference existing sources
1386    /// - Remote dependencies must specify version constraints
1387    /// - Local dependencies cannot have version constraints
1388    /// - No version conflicts between dependencies with the same name
1389    ///
1390    /// ## Path Validation
1391    /// - Local dependency paths are checked for proper format
1392    /// - Remote dependency paths are validated as repository-relative
1393    /// - Path traversal attempts are detected and rejected
1394    ///
1395    /// # Error Types
1396    ///
1397    /// Returns specific error types for different validation failures:
1398    /// - [`crate::core::AgpmError::SourceNotFound`]: Referenced source doesn't exist
1399    /// - [`crate::core::AgpmError::ManifestValidationError`]: General validation failures
1400    /// - Context errors for specific issues with actionable suggestions
1401    ///
1402    /// # Examples
1403    ///
1404    /// ```rust,no_run
1405    /// use agpm_cli::manifest::{Manifest, ResourceDependency, DetailedDependency};
1406    ///
1407    /// let mut manifest = Manifest::new();
1408    ///
1409    /// // This will pass validation (local dependency)
1410    /// manifest.add_dependency(
1411    ///     "local".to_string(),
1412    ///     ResourceDependency::Simple("../local/helper.md".to_string()),
1413    ///     true
1414    /// );
1415    /// assert!(manifest.validate().is_ok());
1416    ///
1417    /// // This will fail validation (missing source)
1418    /// manifest.add_dependency(
1419    ///     "remote".to_string(),
1420    ///     ResourceDependency::Detailed(Box::new(DetailedDependency {
1421    ///         source: Some("missing".to_string()),
1422    ///         path: "agent.md".to_string(),
1423    ///         version: Some("v1.0.0".to_string()),
1424    ///         branch: None,
1425    ///         rev: None,
1426    ///         command: None,
1427    ///         args: None,
1428    ///         target: None,
1429    ///         filename: None,
1430    ///         dependencies: None,
1431    ///         tool: Some("claude-code".to_string()),
1432    ///         flatten: None,
1433    ///         install: None,
1434    ///         template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
1435    ///     })),
1436    ///     true
1437    /// );
1438    /// assert!(manifest.validate().is_err());
1439    /// ```
1440    ///
1441    /// # Security Considerations
1442    ///
1443    /// This method enforces critical security rules:
1444    /// - Prevents credential leakage in version-controlled files
1445    /// - Blocks path traversal attacks in local dependencies
1446    /// - Validates URL schemes to prevent protocol confusion
1447    /// - Checks for malicious patterns in dependency specifications
1448    ///
1449    /// # Performance
1450    ///
1451    /// Validation is designed to be fast and is safe to call frequently.
1452    /// Complex validations (like network connectivity) are not performed
1453    /// here - those are handled during dependency resolution.
1454    pub fn validate(&self) -> Result<()> {
1455        // Validate artifact type names
1456        for artifact_type in self.get_tools_config().types.keys() {
1457            if artifact_type.contains('/') || artifact_type.contains('\\') {
1458                return Err(crate::core::AgpmError::ManifestValidationError {
1459                    reason: format!(
1460                        "Artifact type name '{artifact_type}' cannot contain path separators ('/' or '\\\\'). \n\
1461                        Artifact type names must be simple identifiers without special characters."
1462                    ),
1463                }
1464                .into());
1465            }
1466
1467            // Also check for other potentially problematic characters
1468            if artifact_type.contains("..") {
1469                return Err(crate::core::AgpmError::ManifestValidationError {
1470                    reason: format!(
1471                        "Artifact type name '{artifact_type}' cannot contain '..' (path traversal). \n\
1472                        Artifact type names must be simple identifiers."
1473                    ),
1474                }
1475                .into());
1476            }
1477        }
1478
1479        // Check that all referenced sources exist and dependencies have required fields
1480        for (name, dep) in self.all_dependencies() {
1481            // Check for empty path
1482            if dep.get_path().is_empty() {
1483                return Err(crate::core::AgpmError::ManifestValidationError {
1484                    reason: format!("Missing required field 'path' for dependency '{name}'"),
1485                }
1486                .into());
1487            }
1488
1489            // Validate pattern safety if it's a pattern dependency
1490            if dep.is_pattern() {
1491                crate::pattern::validate_pattern_safety(dep.get_path()).map_err(|e| {
1492                    crate::core::AgpmError::ManifestValidationError {
1493                        reason: format!("Invalid pattern in dependency '{name}': {e}"),
1494                    }
1495                })?;
1496            }
1497
1498            // Check for version when source is specified (non-local dependencies)
1499            if let Some(source) = dep.get_source() {
1500                if !self.sources.contains_key(source) {
1501                    return Err(crate::core::AgpmError::SourceNotFound {
1502                        name: source.to_string(),
1503                    }
1504                    .into());
1505                }
1506
1507                // Check if the source URL is a local path
1508                let source_url = self.sources.get(source).unwrap();
1509                let _is_local_source = source_url.starts_with('/')
1510                    || source_url.starts_with("./")
1511                    || source_url.starts_with("../");
1512
1513                // Git dependencies can optionally have a version (defaults to 'main' if not specified)
1514                // Local path sources don't need versions
1515                // We no longer require versions for Git dependencies - they'll default to 'main'
1516            } else {
1517                // For local path dependencies (no source), version is not allowed
1518                // Skip directory check for pattern dependencies
1519                if !dep.is_pattern() {
1520                    let path = dep.get_path();
1521                    let is_plain_dir =
1522                        path.starts_with('/') || path.starts_with("./") || path.starts_with("../");
1523
1524                    if is_plain_dir && dep.get_version().is_some() {
1525                        return Err(crate::core::AgpmError::ManifestValidationError {
1526                            reason: format!(
1527                                "Version specified for plain directory dependency '{name}' with path '{path}'. \n\
1528                                Plain directory dependencies do not support versions. \n\
1529                            Remove the 'version' field or use a git source instead."
1530                            ),
1531                        }
1532                        .into());
1533                    }
1534                }
1535            }
1536        }
1537
1538        // Check for version conflicts (same dependency name with different versions)
1539        let mut seen_deps: std::collections::HashMap<String, String> =
1540            std::collections::HashMap::new();
1541        for (name, dep) in self.all_dependencies() {
1542            if let Some(version) = dep.get_version() {
1543                if let Some(existing_version) = seen_deps.get(name) {
1544                    if existing_version != version {
1545                        return Err(crate::core::AgpmError::ManifestValidationError {
1546                            reason: format!(
1547                                "Version conflict for dependency '{name}': found versions '{existing_version}' and '{version}'"
1548                            ),
1549                        }
1550                        .into());
1551                    }
1552                } else {
1553                    seen_deps.insert(name.to_string(), version.to_string());
1554                }
1555            }
1556        }
1557
1558        // Validate URLs in sources
1559        for (name, url) in &self.sources {
1560            // Expand environment variables and home directory in URL
1561            let expanded_url = expand_url(url)?;
1562
1563            if !expanded_url.starts_with("http://")
1564                && !expanded_url.starts_with("https://")
1565                && !expanded_url.starts_with("git@")
1566                && !expanded_url.starts_with("file://")
1567            // Plain directory paths not allowed as sources
1568            && !expanded_url.starts_with('/')
1569            && !expanded_url.starts_with("./")
1570            && !expanded_url.starts_with("../")
1571            {
1572                return Err(crate::core::AgpmError::ManifestValidationError {
1573                    reason: format!("Source '{name}' has invalid URL: '{url}'. Must be HTTP(S), SSH (git@...), or file:// URL"),
1574                }
1575                .into());
1576            }
1577
1578            // Check if plain directory path is used as a source
1579            if expanded_url.starts_with('/')
1580                || expanded_url.starts_with("./")
1581                || expanded_url.starts_with("../")
1582            {
1583                return Err(crate::core::AgpmError::ManifestValidationError {
1584                    reason: format!(
1585                        "Plain directory path '{url}' cannot be used as source '{name}'. \n\
1586                        Sources must be git repositories. Use one of:\n\
1587                        - Remote URL: https://github.com/owner/repo.git\n\
1588                        - Local git repo: file:///absolute/path/to/repo\n\
1589                        - Or use direct path dependencies without a source"
1590                    ),
1591                }
1592                .into());
1593            }
1594        }
1595
1596        // Check for case-insensitive conflicts on all platforms
1597        // This ensures manifests are portable across different filesystems
1598        // Even though Linux supports case-sensitive files, we reject conflicts
1599        // to ensure the manifest works on Windows and macOS too
1600        let mut normalized_names: std::collections::HashSet<String> =
1601            std::collections::HashSet::new();
1602
1603        for (name, _) in self.all_dependencies() {
1604            let normalized = name.to_lowercase();
1605            if !normalized_names.insert(normalized.clone()) {
1606                // Find the original conflicting name
1607                for (other_name, _) in self.all_dependencies() {
1608                    if other_name != name && other_name.to_lowercase() == normalized {
1609                        return Err(crate::core::AgpmError::ManifestValidationError {
1610                            reason: format!(
1611                                "Case conflict: '{name}' and '{other_name}' would map to the same file on case-insensitive filesystems. To ensure portability across platforms, resource names must be case-insensitively unique."
1612                            ),
1613                        }
1614                        .into());
1615                    }
1616                }
1617            }
1618        }
1619
1620        // Validate artifact types and resource type support
1621        for resource_type in crate::core::ResourceType::all() {
1622            if let Some(deps) = self.get_dependencies(*resource_type) {
1623                for (name, dep) in deps {
1624                    // Get tool from dependency (defaults based on resource type)
1625                    let tool_string = dep
1626                        .get_tool()
1627                        .map(|s| s.to_string())
1628                        .unwrap_or_else(|| self.get_default_tool(*resource_type));
1629                    let tool = tool_string.as_str();
1630
1631                    // Check if tool is configured
1632                    if self.get_tool_config(tool).is_none() {
1633                        return Err(crate::core::AgpmError::ManifestValidationError {
1634                            reason: format!(
1635                                "Unknown tool '{tool}' for dependency '{name}'.\n\
1636                                Available types: {}\n\
1637                                Configure custom types in [tools] section or use a standard type.",
1638                                self.get_tools_config()
1639                                    .types
1640                                    .keys()
1641                                    .map(|s| format!("'{s}'"))
1642                                    .collect::<Vec<_>>()
1643                                    .join(", ")
1644                            ),
1645                        }
1646                        .into());
1647                    }
1648
1649                    // Check if resource type is supported by this tool
1650                    if !self.is_resource_supported(tool, *resource_type) {
1651                        let artifact_config = self.get_tool_config(tool).unwrap();
1652                        let resource_plural = resource_type.to_plural();
1653
1654                        // Check if this is a malformed configuration (resource exists but not properly configured)
1655                        let is_malformed = artifact_config.resources.contains_key(resource_plural);
1656
1657                        let supported_types: Vec<String> = artifact_config
1658                            .resources
1659                            .iter()
1660                            .filter(|(_, res_config)| {
1661                                res_config.path.is_some() || res_config.merge_target.is_some()
1662                            })
1663                            .map(|(s, _)| s.to_string())
1664                            .collect();
1665
1666                        // Build resource-type-specific suggestions
1667                        let mut suggestions = Vec::new();
1668
1669                        if is_malformed {
1670                            // Resource type exists but is malformed
1671                            suggestions.push(format!(
1672                                "Resource type '{}' is configured for tool '{}' but missing required 'path' or 'merge_target' field",
1673                                resource_plural, tool
1674                            ));
1675
1676                            // Provide specific fix suggestions based on resource type
1677                            match resource_type {
1678                                crate::core::ResourceType::Hook => {
1679                                    suggestions.push("For hooks, add: merge_target = '.claude/settings.local.json'".to_string());
1680                                }
1681                                crate::core::ResourceType::McpServer => {
1682                                    suggestions.push(
1683                                        "For MCP servers, add: merge_target = '.mcp.json'"
1684                                            .to_string(),
1685                                    );
1686                                }
1687                                _ => {
1688                                    suggestions.push(format!(
1689                                        "For {}, add: path = '{}'",
1690                                        resource_plural, resource_plural
1691                                    ));
1692                                }
1693                            }
1694                        } else {
1695                            // Resource type not supported at all
1696                            match resource_type {
1697                                crate::core::ResourceType::Snippet => {
1698                                    suggestions.push("Snippets work best with the 'agpm' tool (shared infrastructure)".to_string());
1699                                    suggestions.push(
1700                                        "Add tool='agpm' to this dependency to use shared snippets"
1701                                            .to_string(),
1702                                    );
1703                                }
1704                                _ => {
1705                                    // Find which tool types DO support this resource type
1706                                    let default_config = ToolsConfig::default();
1707                                    let tools_config =
1708                                        self.tools.as_ref().unwrap_or(&default_config);
1709                                    let supporting_types: Vec<String> = tools_config
1710                                        .types
1711                                        .iter()
1712                                        .filter(|(_, config)| {
1713                                            config.resources.contains_key(resource_plural)
1714                                                && config
1715                                                    .resources
1716                                                    .get(resource_plural)
1717                                                    .map(|res| {
1718                                                        res.path.is_some()
1719                                                            || res.merge_target.is_some()
1720                                                    })
1721                                                    .unwrap_or(false)
1722                                        })
1723                                        .map(|(type_name, _)| format!("'{}'", type_name))
1724                                        .collect();
1725
1726                                    if !supporting_types.is_empty() {
1727                                        suggestions.push(format!(
1728                                            "This resource type is supported by tools: {}",
1729                                            supporting_types.join(", ")
1730                                        ));
1731                                    }
1732                                }
1733                            }
1734                        }
1735
1736                        let mut reason = if is_malformed {
1737                            format!(
1738                                "Resource type '{}' is improperly configured for tool '{}' for dependency '{}'.\n\n",
1739                                resource_plural, tool, name
1740                            )
1741                        } else {
1742                            format!(
1743                                "Resource type '{}' is not supported by tool '{}' for dependency '{}'.\n\n",
1744                                resource_plural, tool, name
1745                            )
1746                        };
1747
1748                        reason.push_str(&format!(
1749                            "Tool '{}' properly supports: {}\n\n",
1750                            tool,
1751                            supported_types.join(", ")
1752                        ));
1753
1754                        if !suggestions.is_empty() {
1755                            reason.push_str("💡 Suggestions:\n");
1756                            for suggestion in &suggestions {
1757                                reason.push_str(&format!("  • {}\n", suggestion));
1758                            }
1759                            reason.push('\n');
1760                        }
1761
1762                        reason.push_str(
1763                            "You can fix this by:\n\
1764                            1. Changing the 'tool' field to a supported tool\n\
1765                            2. Using a different resource type\n\
1766                            3. Removing this dependency from your manifest",
1767                        );
1768
1769                        return Err(crate::core::AgpmError::ManifestValidationError {
1770                            reason,
1771                        }
1772                        .into());
1773                    }
1774                }
1775            }
1776        }
1777
1778        // Validate patches reference valid aliases
1779        self.validate_patches()?;
1780
1781        Ok(())
1782    }
1783
1784    /// Validate that patches reference valid manifest aliases.
1785    ///
1786    /// This method checks that all patch aliases correspond to actual dependencies
1787    /// defined in the manifest. Patches for non-existent aliases are rejected.
1788    ///
1789    /// # Errors
1790    ///
1791    /// Returns an error if a patch references an alias that doesn't exist in the manifest.
1792    fn validate_patches(&self) -> Result<()> {
1793        use crate::core::ResourceType;
1794
1795        // Helper to check if an alias exists for a resource type
1796        let check_patch_aliases = |resource_type: ResourceType,
1797                                   patches: &BTreeMap<String, PatchData>|
1798         -> Result<()> {
1799            let deps = self.get_dependencies(resource_type);
1800
1801            for alias in patches.keys() {
1802                // Check if this alias exists in the manifest
1803                let exists = if let Some(deps) = deps {
1804                    deps.contains_key(alias)
1805                } else {
1806                    false
1807                };
1808
1809                if !exists {
1810                    return Err(crate::core::AgpmError::ManifestValidationError {
1811                            reason: format!(
1812                                "Patch references unknown alias '{alias}' in [patch.{}] section.\n\
1813                                The alias must be defined in [{}] section of agpm.toml.\n\
1814                                To patch a transitive dependency, first add it explicitly to your manifest.",
1815                                resource_type.to_plural(),
1816                                resource_type.to_plural()
1817                            ),
1818                        }
1819                        .into());
1820                }
1821            }
1822            Ok(())
1823        };
1824
1825        // Validate patches for each resource type
1826        check_patch_aliases(ResourceType::Agent, &self.patches.agents)?;
1827        check_patch_aliases(ResourceType::Snippet, &self.patches.snippets)?;
1828        check_patch_aliases(ResourceType::Command, &self.patches.commands)?;
1829        check_patch_aliases(ResourceType::Script, &self.patches.scripts)?;
1830        check_patch_aliases(ResourceType::McpServer, &self.patches.mcp_servers)?;
1831        check_patch_aliases(ResourceType::Hook, &self.patches.hooks)?;
1832
1833        Ok(())
1834    }
1835
1836    /// Get all dependencies from both agents and snippets sections.
1837    ///
1838    /// Returns a vector of tuples containing dependency names and their
1839    /// specifications. This is useful for iteration over all dependencies
1840    /// without needing to handle agents and snippets separately.
1841    ///
1842    /// # Return Value
1843    ///
1844    /// Each tuple contains:
1845    /// - `&str`: The dependency name (key from TOML)
1846    /// - `&ResourceDependency`: The dependency specification
1847    ///
1848    /// # Examples
1849    ///
1850    /// ```rust,no_run
1851    /// use agpm_cli::manifest::Manifest;
1852    ///
1853    /// let manifest = Manifest::new();
1854    /// // ... add some dependencies
1855    ///
1856    /// for (name, dep) in manifest.all_dependencies() {
1857    ///     println!("Dependency: {} -> {}", name, dep.get_path());
1858    ///     if let Some(source) = dep.get_source() {
1859    ///         println!("  Source: {}", source);
1860    ///     }
1861    /// }
1862    /// ```
1863    ///
1864    /// # Order
1865    ///
1866    /// Dependencies are returned in the order they appear in the underlying
1867    /// `HashMaps` (agents first, then snippets, then commands), which means the order is not
1868    /// guaranteed to be stable across runs.
1869    /// Get dependencies for a specific resource type
1870    ///
1871    /// Returns the `HashMap` of dependencies for the specified resource type.
1872    /// Note: MCP servers return None as they use a different dependency type.
1873    pub const fn get_dependencies(
1874        &self,
1875        resource_type: crate::core::ResourceType,
1876    ) -> Option<&HashMap<String, ResourceDependency>> {
1877        use crate::core::ResourceType;
1878        match resource_type {
1879            ResourceType::Agent => Some(&self.agents),
1880            ResourceType::Snippet => Some(&self.snippets),
1881            ResourceType::Command => Some(&self.commands),
1882            ResourceType::Script => Some(&self.scripts),
1883            ResourceType::Hook => Some(&self.hooks),
1884            ResourceType::McpServer => Some(&self.mcp_servers),
1885        }
1886    }
1887
1888    /// Get mutable dependencies for a specific resource type
1889    ///
1890    /// Returns a mutable reference to the `HashMap` of dependencies for the specified resource type.
1891    #[must_use]
1892    pub fn get_dependencies_mut(
1893        &mut self,
1894        resource_type: crate::core::ResourceType,
1895    ) -> Option<&mut HashMap<String, ResourceDependency>> {
1896        use crate::core::ResourceType;
1897        match resource_type {
1898            ResourceType::Agent => Some(&mut self.agents),
1899            ResourceType::Snippet => Some(&mut self.snippets),
1900            ResourceType::Command => Some(&mut self.commands),
1901            ResourceType::Script => Some(&mut self.scripts),
1902            ResourceType::Hook => Some(&mut self.hooks),
1903            ResourceType::McpServer => Some(&mut self.mcp_servers),
1904        }
1905    }
1906
1907    /// Get the tools configuration, returning default if not specified.
1908    ///
1909    /// This method provides access to the tool configurations which define
1910    /// where resources are installed for different tools (claude-code, opencode, agpm).
1911    ///
1912    /// Returns the configured tools or the default configuration if not specified.
1913    pub fn get_tools_config(&self) -> &ToolsConfig {
1914        self.tools.as_ref().unwrap_or_else(|| {
1915            // Return a static default - this is safe because ToolsConfig::default() is deterministic
1916            static DEFAULT: std::sync::OnceLock<ToolsConfig> = std::sync::OnceLock::new();
1917            DEFAULT.get_or_init(ToolsConfig::default)
1918        })
1919    }
1920
1921    /// Get configuration for a specific tool type.
1922    ///
1923    /// Returns None if the tool is not configured.
1924    pub fn get_tool_config(&self, tool: &str) -> Option<&ArtifactTypeConfig> {
1925        self.get_tools_config().types.get(tool)
1926    }
1927
1928    /// Get the installation path for a resource within a tool.
1929    ///
1930    /// Returns the full installation directory path by combining:
1931    /// - Tool's base directory (e.g., ".claude", ".opencode")
1932    /// - Resource type's subdirectory (e.g., "agents", "command")
1933    ///
1934    /// Returns None if:
1935    /// - The tool is not configured
1936    /// - The resource type is not supported by this tool
1937    /// - The resource has no configured path (special handling like MCP merge)
1938    pub fn get_artifact_resource_path(
1939        &self,
1940        tool: &str,
1941        resource_type: crate::core::ResourceType,
1942    ) -> Option<std::path::PathBuf> {
1943        let artifact_config = self.get_tool_config(tool)?;
1944        let resource_config = artifact_config.resources.get(resource_type.to_plural())?;
1945
1946        resource_config.path.as_ref().map(|subdir| artifact_config.path.join(subdir))
1947    }
1948
1949    /// Get the merge target configuration file path for a resource type.
1950    ///
1951    /// Returns the path to the configuration file where resources of this type
1952    /// should be merged (e.g., hooks, MCP servers). Returns None if the resource
1953    /// type doesn't use merge targets or if the tool doesn't support this resource type.
1954    ///
1955    /// # Arguments
1956    ///
1957    /// * `tool` - The tool name (e.g., "claude-code", "opencode")
1958    /// * `resource_type` - The resource type to look up
1959    ///
1960    /// # Returns
1961    ///
1962    /// The merge target path if configured, otherwise None.
1963    ///
1964    /// # Examples
1965    ///
1966    /// ```rust,no_run
1967    /// use agpm_cli::manifest::Manifest;
1968    /// use agpm_cli::core::ResourceType;
1969    ///
1970    /// let manifest = Manifest::new();
1971    ///
1972    /// // Hooks merge into .claude/settings.local.json
1973    /// let hook_target = manifest.get_merge_target("claude-code", ResourceType::Hook);
1974    /// assert_eq!(hook_target, Some(".claude/settings.local.json".into()));
1975    ///
1976    /// // MCP servers merge into .mcp.json for claude-code
1977    /// let mcp_target = manifest.get_merge_target("claude-code", ResourceType::McpServer);
1978    /// assert_eq!(mcp_target, Some(".mcp.json".into()));
1979    ///
1980    /// // MCP servers merge into .opencode/opencode.json for opencode
1981    /// let opencode_mcp = manifest.get_merge_target("opencode", ResourceType::McpServer);
1982    /// assert_eq!(opencode_mcp, Some(".opencode/opencode.json".into()));
1983    /// ```
1984    pub fn get_merge_target(
1985        &self,
1986        tool: &str,
1987        resource_type: crate::core::ResourceType,
1988    ) -> Option<PathBuf> {
1989        let artifact_config = self.get_tool_config(tool)?;
1990        let resource_config = artifact_config.resources.get(resource_type.to_plural())?;
1991
1992        resource_config.merge_target.as_ref().map(PathBuf::from)
1993    }
1994
1995    /// Check if a resource type is supported by a tool.
1996    ///
1997    /// A resource type is considered supported if it has either:
1998    /// - A configured installation path (for file-based resources)
1999    /// - A configured merge target (for resources that merge into config files)
2000    ///
2001    /// Returns true if the tool has valid configuration for the given resource type.
2002    pub fn is_resource_supported(
2003        &self,
2004        tool: &str,
2005        resource_type: crate::core::ResourceType,
2006    ) -> bool {
2007        self.get_tool_config(tool)
2008            .and_then(|config| config.resources.get(resource_type.to_plural()))
2009            .map(|res_config| res_config.path.is_some() || res_config.merge_target.is_some())
2010            .unwrap_or(false)
2011    }
2012
2013    /// Returns all dependencies from all resource types.
2014    ///
2015    /// This method collects dependencies from agents, snippets, commands,
2016    /// scripts, hooks, and MCP servers into a single vector. It's commonly used for:
2017    /// - Manifest validation across all dependency types
2018    /// - Dependency resolution operations
2019    /// - Generating reports of all configured dependencies
2020    /// - Bulk operations on all dependencies
2021    ///
2022    /// # Returns
2023    ///
2024    /// A vector of tuples containing the dependency name and its configuration.
2025    /// Each tuple is `(name, dependency)` where:
2026    /// - `name`: The dependency name as specified in the manifest
2027    /// - `dependency`: Reference to the [`ResourceDependency`] configuration
2028    ///
2029    /// The order follows the resource type order defined in [`crate::core::ResourceType::all()`].
2030    ///
2031    /// # Examples
2032    ///
2033    /// ```rust,no_run
2034    /// # use agpm_cli::manifest::Manifest;
2035    /// # let manifest = Manifest::new();
2036    /// for (name, dep) in manifest.all_dependencies() {
2037    ///     println!("Dependency: {} -> {}", name, dep.get_path());
2038    ///     if let Some(source) = dep.get_source() {
2039    ///         println!("  Source: {}", source);
2040    ///     }
2041    /// }
2042    /// ```
2043    #[must_use]
2044    pub fn all_dependencies(&self) -> Vec<(&str, &ResourceDependency)> {
2045        let mut deps = Vec::new();
2046
2047        // Use ResourceType::all() to iterate through all resource types
2048        for resource_type in crate::core::ResourceType::all() {
2049            if let Some(type_deps) = self.get_dependencies(*resource_type) {
2050                // CRITICAL: Sort for deterministic iteration order
2051                let mut sorted_deps: Vec<_> = type_deps.iter().collect();
2052                sorted_deps.sort_by_key(|(name, _)| name.as_str());
2053
2054                for (name, dep) in sorted_deps {
2055                    deps.push((name.as_str(), dep));
2056                }
2057            }
2058        }
2059
2060        deps
2061    }
2062
2063    /// Get all dependencies including MCP servers.
2064    ///
2065    /// All resource types now use standard `ResourceDependency`, so no conversion needed.
2066    #[must_use]
2067    pub fn all_dependencies_with_mcp(
2068        &self,
2069    ) -> Vec<(&str, std::borrow::Cow<'_, ResourceDependency>)> {
2070        let mut deps = Vec::new();
2071
2072        // Use ResourceType::all() to iterate through all resource types
2073        for resource_type in crate::core::ResourceType::all() {
2074            if let Some(type_deps) = self.get_dependencies(*resource_type) {
2075                // CRITICAL: Sort for deterministic iteration order
2076                let mut sorted_deps: Vec<_> = type_deps.iter().collect();
2077                sorted_deps.sort_by_key(|(name, _)| name.as_str());
2078
2079                for (name, dep) in sorted_deps {
2080                    deps.push((name.as_str(), std::borrow::Cow::Borrowed(dep)));
2081                }
2082            }
2083        }
2084
2085        deps
2086    }
2087
2088    /// Get all dependencies with their resource types.
2089    ///
2090    /// Returns a vector of tuples containing the dependency name, dependency details,
2091    /// and the resource type. This preserves type information that is lost in
2092    /// `all_dependencies_with_mcp()`.
2093    ///
2094    /// This is used by the resolver to correctly type transitive dependencies without
2095    /// falling back to manifest section order lookups.
2096    ///
2097    /// Dependencies for disabled tools are automatically filtered out.
2098    pub fn all_dependencies_with_types(
2099        &self,
2100    ) -> Vec<(&str, std::borrow::Cow<'_, ResourceDependency>, crate::core::ResourceType)> {
2101        let mut deps = Vec::new();
2102
2103        // Use ResourceType::all() to iterate through all resource types
2104        for resource_type in crate::core::ResourceType::all() {
2105            if let Some(type_deps) = self.get_dependencies(*resource_type) {
2106                // CRITICAL: Sort dependencies for deterministic iteration order!
2107                // HashMap iteration is non-deterministic, so we must sort by name
2108                // to ensure consistent lockfile generation across runs.
2109                let mut sorted_deps: Vec<_> = type_deps.iter().collect();
2110                sorted_deps.sort_by_key(|(name, _)| name.as_str());
2111
2112                for (name, dep) in sorted_deps {
2113                    // Determine the tool for this dependency
2114                    let tool_string = dep
2115                        .get_tool()
2116                        .map(|s| s.to_string())
2117                        .unwrap_or_else(|| self.get_default_tool(*resource_type));
2118                    let tool = tool_string.as_str();
2119
2120                    // Check if the tool is enabled
2121                    if let Some(tool_config) = self.get_tools_config().types.get(tool) {
2122                        if !tool_config.enabled {
2123                            // Skip dependencies for disabled tools
2124                            tracing::debug!(
2125                                "Skipping dependency '{}' for disabled tool '{}'",
2126                                name,
2127                                tool
2128                            );
2129                            continue;
2130                        }
2131                    }
2132
2133                    // Ensure the tool is set on the dependency (apply default if not explicitly set)
2134                    let dep_with_tool = if dep.get_tool().is_none() {
2135                        tracing::debug!(
2136                            "Setting default tool '{}' for dependency '{}' (type: {:?})",
2137                            tool,
2138                            name,
2139                            resource_type
2140                        );
2141                        // Need to set the tool - create a modified copy
2142                        let mut dep_owned = dep.clone();
2143                        dep_owned.set_tool(Some(tool_string.clone()));
2144                        std::borrow::Cow::Owned(dep_owned)
2145                    } else {
2146                        std::borrow::Cow::Borrowed(dep)
2147                    };
2148
2149                    deps.push((name.as_str(), dep_with_tool, *resource_type));
2150                }
2151            }
2152        }
2153
2154        deps
2155    }
2156
2157    /// Check if a dependency with the given name exists in any section.
2158    ///
2159    /// Searches the `[agents]`, `[snippets]`, and `[commands]` sections for a dependency
2160    /// with the specified name. This is useful for avoiding duplicate names
2161    /// across different resource types.
2162    ///
2163    /// # Examples
2164    ///
2165    /// ```rust,no_run
2166    /// use agpm_cli::manifest::{Manifest, ResourceDependency};
2167    ///
2168    /// let mut manifest = Manifest::new();
2169    /// manifest.add_dependency(
2170    ///     "helper".to_string(),
2171    ///     ResourceDependency::Simple("../helper.md".to_string()),
2172    ///     true  // is_agent
2173    /// );
2174    ///
2175    /// assert!(manifest.has_dependency("helper"));
2176    /// assert!(!manifest.has_dependency("nonexistent"));
2177    /// ```
2178    ///
2179    /// # Performance
2180    ///
2181    /// This method performs two `HashMap` lookups, so it's O(1) on average.
2182    #[must_use]
2183    pub fn has_dependency(&self, name: &str) -> bool {
2184        self.agents.contains_key(name)
2185            || self.snippets.contains_key(name)
2186            || self.commands.contains_key(name)
2187    }
2188
2189    /// Get a dependency by name from any section.
2190    ///
2191    /// Searches both the `[agents]` and `[snippets]` sections for a dependency
2192    /// with the specified name, returning the first match found. Agents are
2193    /// searched before snippets.
2194    ///
2195    /// # Examples
2196    ///
2197    /// ```rust,no_run
2198    /// use agpm_cli::manifest::{Manifest, ResourceDependency};
2199    ///
2200    /// let mut manifest = Manifest::new();
2201    /// manifest.add_dependency(
2202    ///     "helper".to_string(),
2203    ///     ResourceDependency::Simple("../helper.md".to_string()),
2204    ///     true  // is_agent
2205    /// );
2206    ///
2207    /// if let Some(dep) = manifest.get_dependency("helper") {
2208    ///     println!("Found dependency: {}", dep.get_path());
2209    /// }
2210    /// ```
2211    ///
2212    /// # Search Order
2213    ///
2214    /// Dependencies are searched in this order:
2215    /// 1. `[agents]` section
2216    /// 2. `[snippets]` section
2217    /// 3. `[commands]` section
2218    ///
2219    /// If the same name exists in multiple sections, the first match is returned.
2220    #[must_use]
2221    pub fn get_dependency(&self, name: &str) -> Option<&ResourceDependency> {
2222        self.agents
2223            .get(name)
2224            .or_else(|| self.snippets.get(name))
2225            .or_else(|| self.commands.get(name))
2226    }
2227
2228    /// Find a dependency by name from any section (alias for `get_dependency`).
2229    ///
2230    /// Searches the `[agents]`, `[snippets]`, and `[commands]` sections for a dependency
2231    /// with the specified name, returning the first match found.
2232    ///
2233    /// # Examples
2234    ///
2235    /// ```rust,no_run
2236    /// use agpm_cli::manifest::{Manifest, ResourceDependency};
2237    ///
2238    /// let mut manifest = Manifest::new();
2239    /// manifest.add_dependency(
2240    ///     "helper".to_string(),
2241    ///     ResourceDependency::Simple("../helper.md".to_string()),
2242    ///     true  // is_agent
2243    /// );
2244    ///
2245    /// if let Some(dep) = manifest.find_dependency("helper") {
2246    ///     println!("Found dependency: {}", dep.get_path());
2247    /// }
2248    /// ```
2249    pub fn find_dependency(&self, name: &str) -> Option<&ResourceDependency> {
2250        self.get_dependency(name)
2251    }
2252
2253    /// Add or update a source repository in the `[sources]` section.
2254    ///
2255    /// Sources map convenient names to Git repository URLs. These names can
2256    /// then be referenced in dependency specifications to avoid repeating
2257    /// long URLs throughout the manifest.
2258    ///
2259    /// # Parameters
2260    ///
2261    /// - `name`: Short, convenient name for the source (e.g., "official", "community")
2262    /// - `url`: Git repository URL (HTTPS, SSH, or file:// protocol)
2263    ///
2264    /// # URL Validation
2265    ///
2266    /// The URL is not validated when added - validation occurs during
2267    /// [`Self::validate`]. Supported URL formats:
2268    /// - `https://github.com/owner/repo.git`
2269    /// - `git@github.com:owner/repo.git`
2270    /// - `file:///absolute/path/to/repo`
2271    /// - `file:///path/to/local/repo`
2272    ///
2273    /// # Examples
2274    ///
2275    /// ```rust,no_run
2276    /// use agpm_cli::manifest::Manifest;
2277    ///
2278    /// let mut manifest = Manifest::new();
2279    ///
2280    /// // Add public repository
2281    /// manifest.add_source(
2282    ///     "community".to_string(),
2283    ///     "https://github.com/claude-community/resources.git".to_string()
2284    /// );
2285    ///
2286    /// // Add private repository (SSH)
2287    /// manifest.add_source(
2288    ///     "private".to_string(),
2289    ///     "git@github.com:company/private-resources.git".to_string()
2290    /// );
2291    ///
2292    /// // Add local repository
2293    /// manifest.add_source(
2294    ///     "local".to_string(),
2295    ///     "file:///home/user/my-resources".to_string()
2296    /// );
2297    /// ```
2298    ///
2299    /// # Security Note
2300    ///
2301    /// Never include authentication tokens in the URL. Use SSH keys or
2302    /// configure authentication globally in `~/.agpm/config.toml`.
2303    pub fn add_source(&mut self, name: String, url: String) {
2304        self.sources.insert(name, url);
2305    }
2306
2307    /// Add or update a dependency in the appropriate section.
2308    ///
2309    /// Adds the dependency to either the `[agents]`, `[snippets]`, or `[commands]` section
2310    /// based on the `is_agent` parameter. If a dependency with the same name
2311    /// already exists in the target section, it will be replaced.
2312    ///
2313    /// **Note**: This method is deprecated in favor of [`Self::add_typed_dependency`]
2314    /// which provides explicit control over resource types.
2315    ///
2316    /// # Parameters
2317    ///
2318    /// - `name`: Unique name for the dependency within its section
2319    /// - `dep`: The dependency specification (Simple or Detailed)
2320    /// - `is_agent`: If true, adds to `[agents]`; if false, adds to `[snippets]`
2321    ///   (Note: Use [`Self::add_typed_dependency`] for commands and other resource types)
2322    ///
2323    /// # Validation
2324    ///
2325    /// The dependency is not validated when added - validation occurs during
2326    /// [`Self::validate`]. This allows for building manifests incrementally
2327    /// before all sources are defined.
2328    ///
2329    /// # Examples
2330    ///
2331    /// ```rust,no_run
2332    /// use agpm_cli::manifest::{Manifest, ResourceDependency, DetailedDependency};
2333    ///
2334    /// let mut manifest = Manifest::new();
2335    ///
2336    /// // Add local agent dependency
2337    /// manifest.add_dependency(
2338    ///     "helper".to_string(),
2339    ///     ResourceDependency::Simple("../local/helper.md".to_string()),
2340    ///     true  // is_agent = true
2341    /// );
2342    ///
2343    /// // Add remote snippet dependency
2344    /// manifest.add_dependency(
2345    ///     "utils".to_string(),
2346    ///     ResourceDependency::Detailed(Box::new(DetailedDependency {
2347    ///         source: Some("community".to_string()),
2348    ///         path: "snippets/utils.md".to_string(),
2349    ///         version: Some("v1.0.0".to_string()),
2350    ///         branch: None,
2351    ///         rev: None,
2352    ///         command: None,
2353    ///         args: None,
2354    ///         target: None,
2355    ///         filename: None,
2356    ///         dependencies: None,
2357    ///         tool: Some("claude-code".to_string()),
2358    ///         flatten: None,
2359    ///         install: None,
2360    ///         template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
2361    ///     })),
2362    ///     false  // is_agent = false (snippet)
2363    /// );
2364    /// ```
2365    ///
2366    /// # Name Conflicts
2367    ///
2368    /// This method allows the same dependency name to exist in both the
2369    /// `[agents]` and `[snippets]` sections. However, some operations like
2370    /// [`Self::get_dependency`] will prefer agents over snippets when
2371    /// searching by name.
2372    pub fn add_dependency(&mut self, name: String, dep: ResourceDependency, is_agent: bool) {
2373        if is_agent {
2374            self.agents.insert(name, dep);
2375        } else {
2376            self.snippets.insert(name, dep);
2377        }
2378    }
2379
2380    /// Add or update a dependency with specific resource type.
2381    ///
2382    /// This is the preferred method for adding dependencies as it explicitly
2383    /// specifies the resource type using the `ResourceType` enum.
2384    ///
2385    /// # Examples
2386    ///
2387    /// ```rust,no_run
2388    /// use agpm_cli::manifest::{Manifest, ResourceDependency};
2389    /// use agpm_cli::core::ResourceType;
2390    ///
2391    /// let mut manifest = Manifest::new();
2392    ///
2393    /// // Add command dependency
2394    /// manifest.add_typed_dependency(
2395    ///     "build".to_string(),
2396    ///     ResourceDependency::Simple("../commands/build.md".to_string()),
2397    ///     ResourceType::Command
2398    /// );
2399    /// ```
2400    pub fn add_typed_dependency(
2401        &mut self,
2402        name: String,
2403        dep: ResourceDependency,
2404        resource_type: crate::core::ResourceType,
2405    ) {
2406        match resource_type {
2407            crate::core::ResourceType::Agent => {
2408                self.agents.insert(name, dep);
2409            }
2410            crate::core::ResourceType::Snippet => {
2411                self.snippets.insert(name, dep);
2412            }
2413            crate::core::ResourceType::Command => {
2414                self.commands.insert(name, dep);
2415            }
2416            crate::core::ResourceType::McpServer => {
2417                // MCP servers don't use ResourceDependency, they have their own type
2418                // This method shouldn't be called for MCP servers
2419                panic!("Use add_mcp_server() for MCP server dependencies");
2420            }
2421            crate::core::ResourceType::Script => {
2422                self.scripts.insert(name, dep);
2423            }
2424            crate::core::ResourceType::Hook => {
2425                self.hooks.insert(name, dep);
2426            }
2427        }
2428    }
2429
2430    /// Get resource dependencies by type.
2431    ///
2432    /// Returns a reference to the HashMap of dependencies for the specified resource type.
2433    /// This provides a unified interface for accessing different resource collections,
2434    /// similar to `LockFile::get_resources()`.
2435    ///
2436    /// # Examples
2437    ///
2438    /// ```rust,no_run
2439    /// use agpm_cli::manifest::Manifest;
2440    /// use agpm_cli::core::ResourceType;
2441    ///
2442    /// let manifest = Manifest::new();
2443    /// let agents = manifest.get_resources(&ResourceType::Agent);
2444    /// println!("Found {} agent dependencies", agents.len());
2445    /// ```
2446    #[must_use]
2447    pub fn get_resources(
2448        &self,
2449        resource_type: &crate::core::ResourceType,
2450    ) -> &HashMap<String, ResourceDependency> {
2451        use crate::core::ResourceType;
2452        match resource_type {
2453            ResourceType::Agent => &self.agents,
2454            ResourceType::Snippet => &self.snippets,
2455            ResourceType::Command => &self.commands,
2456            ResourceType::Script => &self.scripts,
2457            ResourceType::Hook => &self.hooks,
2458            ResourceType::McpServer => &self.mcp_servers,
2459        }
2460    }
2461
2462    /// Get all resource dependencies across all types.
2463    ///
2464    /// Returns a vector of tuples containing the resource type, manifest key (name),
2465    /// and the dependency specification. This provides a unified way to iterate over
2466    /// all resources regardless of type.
2467    ///
2468    /// # Returns
2469    ///
2470    /// A vector of `(ResourceType, &str, &ResourceDependency)` tuples where:
2471    /// - The first element is the type of resource (Agent, Snippet, etc.)
2472    /// - The second element is the manifest key (the name in the TOML file)
2473    /// - The third element is the resource dependency specification
2474    ///
2475    /// # Examples
2476    ///
2477    /// ```rust,no_run
2478    /// use agpm_cli::manifest::Manifest;
2479    ///
2480    /// let manifest = Manifest::new();
2481    /// let all = manifest.all_resources();
2482    ///
2483    /// for (resource_type, name, dep) in all {
2484    ///     println!("{:?}: {}", resource_type, name);
2485    /// }
2486    /// ```
2487    #[must_use]
2488    pub fn all_resources(&self) -> Vec<(crate::core::ResourceType, &str, &ResourceDependency)> {
2489        use crate::core::ResourceType;
2490
2491        let mut resources = Vec::new();
2492
2493        for resource_type in ResourceType::all() {
2494            let type_resources = self.get_resources(resource_type);
2495            for (name, dep) in type_resources {
2496                resources.push((*resource_type, name.as_str(), dep));
2497            }
2498        }
2499
2500        resources
2501    }
2502
2503    /// Add or update an MCP server configuration.
2504    ///
2505    /// MCP servers now use standard `ResourceDependency` format,
2506    /// pointing to JSON configuration files in source repositories.
2507    ///
2508    /// # Examples
2509    ///
2510    /// ```rust,no_run,ignore
2511    /// use agpm_cli::manifest::{Manifest, ResourceDependency};
2512    ///
2513    /// let mut manifest = Manifest::new();
2514    ///
2515    /// // Add MCP server from source repository
2516    /// manifest.add_mcp_server(
2517    ///     "filesystem".to_string(),
2518    ///     ResourceDependency::Simple("../local/mcp-servers/filesystem.json".to_string())
2519    /// );
2520    /// ```
2521    pub fn add_mcp_server(&mut self, name: String, dependency: ResourceDependency) {
2522        self.mcp_servers.insert(name, dependency);
2523    }
2524}
2525
2526impl Default for Manifest {
2527    fn default() -> Self {
2528        Self::new()
2529    }
2530}