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