agpm_cli/core/
resource.rs

1//! Resource abstractions for AGPM
2//!
3//! This module defines the core resource types and management traits that form the foundation
4//! of AGPM's resource system. Resources are the fundamental units that AGPM manages, installs,
5//! and tracks across different source repositories.
6//!
7//! # Resource Model
8//!
9//! AGPM supports different types of resources, each with specific characteristics:
10//! - **Agents**: AI assistant configurations and prompts
11//! - **Snippets**: Reusable code templates and examples
12//! - **Commands**: Slash commands for AI assistants
13//! - **Scripts**: Executable scripts for automation
14//! - **Hooks**: Event hooks for AI assistant lifecycles
15//! - **MCP Servers**: Model Context Protocol server configurations
16//! - **Skills**: Directory-based resources containing SKILL.md and supporting files
17//!
18//! Most resources are distributed as markdown files (.md) that may contain frontmatter metadata
19//! for configuration and dependency information. Skills are unique in being directory-based
20//! with a required SKILL.md file and optional supporting files.
21//!
22//! # Core Types
23//!
24//! - [`ResourceType`] - Enumeration of supported resource types
25//! - [`Resource`] - Trait defining the interface for all resource types
26//!
27//! # Resource Management
28//!
29//! Resources are defined in the project's `agpm.toml` file and installed to specific
30//! directories based on their type. Scripts and hooks have special handling for
31//! Claude Code integration.
32//!
33//! # Examples
34//!
35//! ## Working with Resource Types
36//!
37//! ```rust,no_run
38//! use agpm_cli::core::ResourceType;
39//! use std::path::Path;
40//!
41//! // Convert strings to resource types
42//! let agent_type: ResourceType = "agent".parse().unwrap();
43//! let snippet_type: ResourceType = "snippet".parse().unwrap();
44//! let script_type: ResourceType = "script".parse().unwrap();
45//! let hook_type: ResourceType = "hook".parse().unwrap();
46//!
47//! // Get default directory names
48//! assert_eq!(agent_type.default_directory(), Some(".claude/agents"));
49//! assert_eq!(snippet_type.default_directory(), Some(".claude/snippets"));
50//! assert_eq!(script_type.default_directory(), Some(".claude/scripts"));
51//! assert_eq!(hook_type.default_directory(), None); // Hooks merged into config, not staged
52//! ```
53//!
54//! ## Serialization Support
55//!
56//! ```rust,no_run
57//! use agpm_cli::core::ResourceType;
58//!
59//! // ResourceType implements Serialize/Deserialize
60//! let agent = ResourceType::Agent;
61//! let json = serde_json::to_string(&agent).unwrap();
62//! assert_eq!(json, "\"agent\"");
63//!
64//! let deserialized: ResourceType = serde_json::from_str(&json).unwrap();
65//! assert_eq!(deserialized, ResourceType::Agent);
66//! ```
67
68use anyhow::Result;
69use serde::{Deserialize, Serialize};
70use std::path::Path;
71
72/// Enumeration of supported resource types in AGPM
73///
74/// This enum defines the different categories of resources that AGPM can manage.
75/// Each resource type has specific characteristics, installation paths, and
76/// manifest file requirements.
77///
78/// # Serialization
79///
80/// `ResourceType` implements [`serde::Serialize`] and [`serde::Deserialize`]
81/// using lowercase string representations ("agent", "snippet") for JSON/TOML
82/// compatibility.
83///
84/// # Resource Type Characteristics
85///
86/// ## Agent
87/// - **Purpose**: AI assistant configurations, prompts, and behavioral definitions
88/// - **Default Directory**: `.claude/agents`
89/// - **Common Use Cases**: Claude Code agents, custom AI assistants, specialized prompts
90///
91/// ## Snippet
92/// - **Purpose**: Reusable code templates, examples, and documentation fragments
93/// - **Default Directory**: `.claude/snippets`
94/// - **Common Use Cases**: Code templates, configuration examples, documentation
95///
96/// ## Script
97/// - **Purpose**: Executable files that can be run by hooks or independently
98/// - **Default Directory**: `.claude/scripts`
99/// - **Common Use Cases**: Validation scripts, automation tools, hook executables
100///
101/// ## Hook
102/// - **Purpose**: Event-based automation configurations for Claude Code
103/// - **Staging**: Merged into `.claude/settings.local.json`, not staged to disk
104/// - **Common Use Cases**: Pre/Post tool use validation, custom event handlers
105///
106/// # Examples
107///
108/// ## Basic Usage
109///
110/// ```rust,no_run
111/// use agpm_cli::core::ResourceType;
112///
113/// let agent = ResourceType::Agent;
114/// let snippet = ResourceType::Snippet;
115///
116/// assert_eq!(agent.to_string(), "agent");
117/// assert_eq!(snippet.to_string(), "snippet");
118/// ```
119///
120/// ## String Parsing
121///
122/// ```rust,no_run
123/// use agpm_cli::core::ResourceType;
124/// use std::str::FromStr;
125///
126/// let agent: ResourceType = "agent".parse().unwrap();
127/// let snippet: ResourceType = "SNIPPET".parse().unwrap(); // Case insensitive
128///
129/// assert_eq!(agent, ResourceType::Agent);
130/// assert_eq!(snippet, ResourceType::Snippet);
131///
132/// // Invalid resource type
133/// assert!("invalid".parse::<ResourceType>().is_err());
134/// ```
135///
136/// ## Directory Names
137///
138/// ```rust,no_run
139/// use agpm_cli::core::ResourceType;
140///
141/// let agent = ResourceType::Agent;
142/// assert_eq!(agent.default_directory(), Some(".claude/agents"));
143///
144/// let snippet = ResourceType::Snippet;
145/// assert_eq!(snippet.default_directory(), Some(".claude/snippets"));
146///
147/// let script = ResourceType::Script;
148/// assert_eq!(script.default_directory(), Some(".claude/scripts"));
149/// ```
150///
151/// ## JSON Serialization
152///
153/// ```rust,no_run
154/// use agpm_cli::core::ResourceType;
155///
156/// let agent = ResourceType::Agent;
157/// let json = serde_json::to_string(&agent).unwrap();
158/// assert_eq!(json, "\"agent\"");
159///
160/// let parsed: ResourceType = serde_json::from_str("\"snippet\"").unwrap();
161/// assert_eq!(parsed, ResourceType::Snippet);
162/// ```
163#[derive(
164    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, Default,
165)]
166#[serde(rename_all = "lowercase")]
167pub enum ResourceType {
168    /// AI assistant configurations and prompts
169    ///
170    /// Agents define AI assistant behavior, including system prompts, specialized
171    /// capabilities, and configuration parameters. They are typically stored as
172    /// markdown files with frontmatter containing metadata.
173    Agent,
174
175    /// Reusable code templates and examples
176    ///
177    /// Snippets contain reusable code fragments, configuration examples, or
178    /// documentation templates that can be shared across projects.
179    #[default]
180    Snippet,
181
182    /// Claude Code commands
183    ///
184    /// Commands define custom slash commands that can be used within Claude Code
185    /// to perform specific actions or automate workflows.
186    Command,
187
188    /// MCP (Model Context Protocol) servers
189    ///
190    /// MCP servers provide integrations with external systems and services,
191    /// allowing Claude Code to access databases, APIs, and other tools.
192    #[serde(rename = "mcp-server")]
193    McpServer,
194
195    /// Executable script files
196    ///
197    /// Scripts are executable files (.sh, .js, .py, etc.) that can be referenced
198    /// by hooks or run independently. They are installed to .claude/scripts/
199    Script,
200
201    /// Hook configuration files
202    ///
203    /// Hooks define event-based automation in Claude Code. They are JSON files
204    /// that configure scripts to run at specific events (`PreToolUse`, `PostToolUse`, etc.)
205    /// and are merged into settings.local.json
206    Hook,
207
208    /// Claude Skills - directory-based resources
209    ///
210    /// Skills are directory-based resources (unlike single-file agents/snippets)
211    /// that contain a `SKILL.md` file plus supporting files (scripts, templates,
212    /// examples). They are installed to `.claude/skills/<name>/` as complete
213    /// directory structures.
214    Skill,
215}
216
217impl ResourceType {
218    /// Get all resource types in a consistent order
219    ///
220    /// Returns an array containing all `ResourceType` variants. This is useful
221    /// for iterating over all resource types when processing manifests, lockfiles,
222    /// or performing batch operations.
223    ///
224    /// The order is guaranteed to be stable: Agent, Snippet, Command, `McpServer`, Script, Hook, Skill
225    ///
226    /// # Examples
227    ///
228    /// ```rust,no_run
229    /// use agpm_cli::core::ResourceType;
230    ///
231    /// // Iterate over all resource types
232    /// for resource_type in ResourceType::all() {
233    ///     println!("Processing {}", resource_type);
234    /// }
235    ///
236    /// // Count total resource types
237    /// assert_eq!(ResourceType::all().len(), 7);
238    /// ```
239    pub const fn all() -> &'static [Self] {
240        &[
241            Self::Agent,
242            Self::Snippet,
243            Self::Command,
244            Self::McpServer,
245            Self::Script,
246            Self::Hook,
247            Self::Skill,
248        ]
249    }
250
251    /// Get the plural form of the resource type.
252    ///
253    /// Returns the plural form used in lockfile dependency references and TOML sections.
254    ///
255    /// # Examples
256    ///
257    /// ```
258    /// use agpm_cli::core::ResourceType;
259    ///
260    /// assert_eq!(ResourceType::Agent.to_plural(), "agents");
261    /// assert_eq!(ResourceType::McpServer.to_plural(), "mcp-servers");
262    /// ```
263    pub const fn to_plural(&self) -> &'static str {
264        match self {
265            Self::Agent => "agents",
266            Self::Snippet => "snippets",
267            Self::Command => "commands",
268            Self::Script => "scripts",
269            Self::Hook => "hooks",
270            Self::McpServer => "mcp-servers",
271            Self::Skill => "skills",
272        }
273    }
274
275    /// Get the default installation directory name for this resource type
276    ///
277    /// Returns the conventional directory name where resources of this type
278    /// are typically installed in AGPM projects.
279    ///
280    /// # Returns
281    ///
282    /// - [`Agent`] → `Some(".claude/agents")`
283    /// - [`Snippet`] → `Some(".claude/snippets")`
284    /// - [`Command`] → `Some(".claude/commands")`
285    /// - [`McpServer`] → `None` (merged into `.mcp.json`, not staged to disk)
286    /// - [`Script`] → `Some(".claude/scripts")`
287    /// - [`Hook`] → `None` (merged into `.claude/settings.local.json`, not staged to disk)
288    /// - [`Skill`] → `Some(".claude/skills")`
289    ///
290    /// # Examples
291    ///
292    /// ```rust,no_run
293    /// use agpm_cli::core::ResourceType;
294    ///
295    /// assert_eq!(ResourceType::Agent.default_directory(), Some(".claude/agents"));
296    /// assert_eq!(ResourceType::Snippet.default_directory(), Some(".claude/snippets"));
297    /// assert_eq!(ResourceType::Command.default_directory(), Some(".claude/commands"));
298    /// assert_eq!(ResourceType::Script.default_directory(), Some(".claude/scripts"));
299    /// // Hook and McpServer return None as they don't stage to disk
300    /// assert_eq!(ResourceType::Hook.default_directory(), None);
301    /// assert_eq!(ResourceType::McpServer.default_directory(), None);
302    /// ```
303    ///
304    /// # Note
305    ///
306    /// This is just the default convention. Users can install resources to any
307    /// directory by specifying custom paths in their manifest files.
308    ///
309    /// [`Agent`]: ResourceType::Agent
310    /// [`Snippet`]: ResourceType::Snippet
311    /// [`Command`]: ResourceType::Command
312    /// [`McpServer`]: ResourceType::McpServer
313    /// [`Script`]: ResourceType::Script
314    /// [`Hook`]: ResourceType::Hook
315    /// [`Skill`]: ResourceType::Skill
316    #[must_use]
317    pub const fn default_directory(&self) -> Option<&str> {
318        match self {
319            Self::Agent => Some(".claude/agents"),
320            Self::Snippet => Some(".claude/snippets"),
321            Self::Command => Some(".claude/commands"),
322            Self::McpServer => None, // Merged into .mcp.json, not staged to disk
323            Self::Script => Some(".claude/scripts"),
324            Self::Hook => None, // Merged into .claude/settings.local.json, not staged to disk
325            Self::Skill => Some(".claude/skills"),
326        }
327    }
328
329    /// Parse a resource type from frontmatter dependency declaration.
330    ///
331    /// Accepts both singular and plural forms (e.g., "agent" or "agents").
332    /// This is specifically designed for parsing dependency declarations in
333    /// resource frontmatter where users may specify either form.
334    ///
335    /// # Arguments
336    ///
337    /// * `s` - The string from frontmatter (e.g., "agents", "snippet")
338    ///
339    /// # Returns
340    ///
341    /// The corresponding ResourceType, or None if not recognized.
342    ///
343    /// # Examples
344    ///
345    /// ```no_run
346    /// use agpm_cli::core::ResourceType;
347    ///
348    /// assert_eq!(ResourceType::from_frontmatter_str("agents"), Some(ResourceType::Agent));
349    /// assert_eq!(ResourceType::from_frontmatter_str("agent"), Some(ResourceType::Agent));
350    /// assert_eq!(ResourceType::from_frontmatter_str("snippets"), Some(ResourceType::Snippet));
351    /// assert_eq!(ResourceType::from_frontmatter_str("snippet"), Some(ResourceType::Snippet));
352    /// assert_eq!(ResourceType::from_frontmatter_str("commands"), Some(ResourceType::Command));
353    /// assert_eq!(ResourceType::from_frontmatter_str("command"), Some(ResourceType::Command));
354    /// assert_eq!(ResourceType::from_frontmatter_str("scripts"), Some(ResourceType::Script));
355    /// assert_eq!(ResourceType::from_frontmatter_str("script"), Some(ResourceType::Script));
356    /// assert_eq!(ResourceType::from_frontmatter_str("hooks"), Some(ResourceType::Hook));
357    /// assert_eq!(ResourceType::from_frontmatter_str("hook"), Some(ResourceType::Hook));
358    /// assert_eq!(ResourceType::from_frontmatter_str("mcp-servers"), Some(ResourceType::McpServer));
359    /// assert_eq!(ResourceType::from_frontmatter_str("mcp-server"), Some(ResourceType::McpServer));
360    /// assert_eq!(ResourceType::from_frontmatter_str("unknown"), None);
361    /// ```
362    pub fn from_frontmatter_str(s: &str) -> Option<Self> {
363        match s {
364            "agents" | "agent" => Some(ResourceType::Agent),
365            "snippets" | "snippet" => Some(ResourceType::Snippet),
366            "commands" | "command" => Some(ResourceType::Command),
367            "hooks" | "hook" => Some(ResourceType::Hook),
368            "scripts" | "script" => Some(ResourceType::Script),
369            "mcp-servers" | "mcp-server" => Some(ResourceType::McpServer),
370            "skills" | "skill" => Some(ResourceType::Skill),
371            _ => None,
372        }
373    }
374
375    /// Get the default tool for this resource type.
376    ///
377    /// Returns the default tool that should be used when no explicit tool is specified
378    /// in the dependency configuration. Different resource types have different defaults:
379    ///
380    /// - Most resources default to "claude-code"
381    /// - Snippets default to "agpm" (shared infrastructure across tools)
382    ///
383    /// # Returns
384    ///
385    /// - [`ResourceType::Agent`] → `"claude-code"`
386    /// - [`ResourceType::Snippet`] → `"agpm"` (shared infrastructure)
387    /// - [`ResourceType::Command`] → `"claude-code"`
388    /// - [`ResourceType::McpServer`] → `"claude-code"`
389    /// - [`ResourceType::Script`] → `"claude-code"`
390    /// - [`ResourceType::Hook`] → `"claude-code"`
391    /// - [`ResourceType::Skill`] → `"claude-code"`
392    ///
393    /// # Examples
394    ///
395    /// ```rust,no_run
396    /// use agpm_cli::core::ResourceType;
397    ///
398    /// assert_eq!(ResourceType::Agent.default_tool(), "claude-code");
399    /// assert_eq!(ResourceType::Snippet.default_tool(), "agpm");
400    /// assert_eq!(ResourceType::Command.default_tool(), "claude-code");
401    /// ```
402    ///
403    /// # Rationale
404    ///
405    /// Snippets are designed to be shared content across multiple tools (Claude Code,
406    /// OpenCode, etc.). The `.agpm/snippets/` location provides shared infrastructure
407    /// that can be referenced by resources from different tools. Therefore, snippets
408    /// default to the "agpm" tool type.
409    ///
410    /// Users can still explicitly set `tool = "claude-code"` for a snippet if they want
411    /// it installed to `.claude/snippets/` instead.
412    #[must_use]
413    pub const fn default_tool(&self) -> &'static str {
414        match self {
415            Self::Snippet => "agpm", // Snippets use shared infrastructure
416            _ => "claude-code",      // All other resources default to claude-code
417        }
418    }
419}
420
421impl std::fmt::Display for ResourceType {
422    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
423        match self {
424            Self::Agent => write!(f, "agent"),
425            Self::Snippet => write!(f, "snippet"),
426            Self::Command => write!(f, "command"),
427            Self::McpServer => write!(f, "mcp-server"),
428            Self::Script => write!(f, "script"),
429            Self::Hook => write!(f, "hook"),
430            Self::Skill => write!(f, "skill"),
431        }
432    }
433}
434
435impl std::str::FromStr for ResourceType {
436    type Err = crate::core::AgpmError;
437
438    fn from_str(s: &str) -> Result<Self, Self::Err> {
439        match s.to_lowercase().as_str() {
440            "agent" | "agents" => Ok(Self::Agent),
441            "snippet" | "snippets" => Ok(Self::Snippet),
442            "command" | "commands" => Ok(Self::Command),
443            "mcp-server" | "mcp-servers" | "mcpserver" | "mcp" => Ok(Self::McpServer),
444            "script" | "scripts" => Ok(Self::Script),
445            "hook" | "hooks" => Ok(Self::Hook),
446            "skill" | "skills" => Ok(Self::Skill),
447            _ => Err(crate::core::AgpmError::InvalidResourceType {
448                resource_type: s.to_string(),
449            }),
450        }
451    }
452}
453
454/// Base trait defining the interface for all AGPM resources
455///
456/// This trait provides a common interface for different types of resources (agents, snippets)
457/// managed by AGPM. It abstracts the core operations that can be performed on any resource,
458/// including validation, installation, and metadata access.
459///
460/// # Design Principles
461///
462/// - **Type Safety**: Each resource has a specific [`ResourceType`]
463/// - **Validation**: Resources can validate their own structure and dependencies
464/// - **Installation**: Resources know how to install themselves to target locations
465/// - **Metadata**: Resources provide structured metadata for tooling and display
466/// - **Flexibility**: Resources can be profiled or configured during installation
467///
468/// # Implementation Requirements
469///
470/// Implementors of this trait should:
471/// - Provide meaningful error messages in validation failures
472/// - Support atomic installation operations (no partial installs on failure)
473/// - Generate deterministic installation paths
474/// - Include rich metadata for resource discovery and management
475///
476/// # Examples
477///
478/// ## Basic Resource Usage Pattern
479///
480/// ```rust,no_run
481/// use agpm_cli::core::{Resource, ResourceType};
482/// use anyhow::Result;
483/// use std::path::Path;
484///
485/// fn process_resource(resource: &dyn Resource) -> Result<()> {
486///     // Get basic information
487///     println!("Processing resource: {}", resource.name());
488///     println!("Type: {}", resource.resource_type());
489///     
490///     if let Some(description) = resource.description() {
491///         println!("Description: {}", description);
492///     }
493///     
494///     // Validate the resource
495///     resource.validate()?;
496///     
497///     // Install to default location
498///     let target = Path::new("./resources");
499///     let install_path = resource.install_path(target);
500///     resource.install(&install_path, None)?;
501///     
502///     Ok(())
503/// }
504/// ```
505///
506/// ## Metadata Extraction
507///
508/// ```rust,no_run
509/// use agpm_cli::core::Resource;
510/// use anyhow::Result;
511///
512/// fn extract_metadata(resource: &dyn Resource) -> Result<()> {
513///     let metadata = resource.metadata()?;
514///     
515///     // Metadata is JSON Value for flexibility
516///     if let Some(version) = metadata.get("version") {
517///         println!("Resource version: {}", version);
518///     }
519///     
520///     if let Some(tags) = metadata.get("tags").and_then(|t| t.as_array()) {
521///         println!("Tags: {:?}", tags);
522///     }
523///     
524///     Ok(())
525/// }
526/// ```
527///
528/// # Trait Object Usage
529///
530/// The trait is object-safe and can be used as a trait object:
531///
532/// ```rust,no_run
533/// use agpm_cli::core::Resource;
534/// use std::any::Any;
535///
536/// fn handle_resource(resource: Box<dyn Resource>) {
537///     println!("Handling resource: {}", resource.name());
538///     
539///     // Can be downcasted to concrete types if needed
540///     let any = resource.as_any();
541///     // ... downcasting logic
542/// }
543/// ```
544pub trait Resource {
545    /// Get the unique name identifier for this resource
546    ///
547    /// The name is used to identify the resource in manifests, lockfiles,
548    /// and CLI operations. It should be unique within a project's namespace.
549    ///
550    /// # Returns
551    ///
552    /// A string slice containing the resource name
553    ///
554    /// # Examples
555    ///
556    /// ```rust,no_run
557    /// use agpm_cli::core::Resource;
558    ///
559    /// fn print_resource_info(resource: &dyn Resource) {
560    ///     println!("Resource name: {}", resource.name());
561    /// }
562    /// ```
563    fn name(&self) -> &str;
564
565    /// Get the resource type classification
566    ///
567    /// Returns the [`ResourceType`] enum value that identifies what kind of
568    /// resource this is (Agent, Snippet, etc.).
569    ///
570    /// # Returns
571    ///
572    /// The [`ResourceType`] for this resource
573    ///
574    /// # Examples
575    ///
576    /// ```rust,no_run
577    /// use agpm_cli::core::{Resource, ResourceType};
578    ///
579    /// fn categorize_resource(resource: &dyn Resource) {
580    ///     match resource.resource_type() {
581    ///         ResourceType::Agent => println!("This is an AI agent"),
582    ///         ResourceType::Snippet => println!("This is a code snippet"),
583    ///         ResourceType::Command => println!("This is a Claude Code command"),
584    ///         ResourceType::McpServer => println!("This is an MCP server"),
585    ///         ResourceType::Script => println!("This is an executable script"),
586    ///         ResourceType::Hook => println!("This is a hook configuration"),
587    ///         ResourceType::Skill => println!("This is a Claude Skill"),
588    ///     }
589    /// }
590    /// ```
591    fn resource_type(&self) -> ResourceType;
592
593    /// Get the human-readable description of this resource
594    ///
595    /// Returns an optional description that explains what the resource does
596    /// or how it should be used. This is typically displayed in resource
597    /// listings and help text.
598    ///
599    /// # Returns
600    ///
601    /// - `Some(description)` if the resource has a description
602    /// - `None` if no description is available
603    ///
604    /// # Examples
605    ///
606    /// ```rust,no_run
607    /// use agpm_cli::core::Resource;
608    ///
609    /// fn show_resource_details(resource: &dyn Resource) {
610    ///     println!("Name: {}", resource.name());
611    ///     if let Some(desc) = resource.description() {
612    ///         println!("Description: {}", desc);
613    ///     } else {
614    ///         println!("No description available");
615    ///     }
616    /// }
617    /// ```
618    fn description(&self) -> Option<&str>;
619
620    /// Validate the resource structure and content
621    ///
622    /// Performs comprehensive validation of the resource including:
623    /// - File structure integrity
624    /// - Content format validation
625    /// - Dependency constraint checking
626    /// - Metadata consistency
627    ///
628    /// # Returns
629    ///
630    /// - `Ok(())` if the resource is valid
631    /// - `Err(error)` with detailed validation failure information
632    ///
633    /// # Examples
634    ///
635    /// ```rust,no_run
636    /// use agpm_cli::core::Resource;
637    /// use anyhow::Result;
638    ///
639    /// fn validate_before_install(resource: &dyn Resource) -> Result<()> {
640    ///     resource.validate()
641    ///         .map_err(|e| anyhow::anyhow!("Resource validation failed: {}", e))?;
642    ///     
643    ///     println!("Resource {} is valid", resource.name());
644    ///     Ok(())
645    /// }
646    /// ```
647    fn validate(&self) -> Result<()>;
648
649    /// Install the resource to the specified target path
650    ///
651    /// Performs the actual installation of the resource files to the target
652    /// location. This operation should be atomic - either it succeeds completely
653    /// or fails without making any changes.
654    ///
655    /// # Arguments
656    ///
657    /// * `target` - The directory path where the resource should be installed
658    /// * `profile` - Optional profile name for customized installation (may be unused)
659    ///
660    /// # Returns
661    ///
662    /// - `Ok(())` if installation succeeds
663    /// - `Err(error)` if installation fails with detailed error information
664    ///
665    /// # Examples
666    ///
667    /// ```rust,no_run
668    /// use agpm_cli::core::Resource;
669    /// use std::path::Path;
670    /// use anyhow::Result;
671    ///
672    /// fn install_resource(resource: &dyn Resource) -> Result<()> {
673    ///     let target = Path::new("./installed-resources");
674    ///     
675    ///     // Validate first
676    ///     resource.validate()?;
677    ///     
678    ///     // Install without profile
679    ///     resource.install(target, None)?;
680    ///     
681    ///     println!("Successfully installed {}", resource.name());
682    ///     Ok(())
683    /// }
684    /// ```
685    fn install(&self, target: &Path, profile: Option<&str>) -> Result<()>;
686
687    /// Calculate the installation path for this resource
688    ///
689    /// Determines where this resource would be installed relative to a base
690    /// directory. This is used for path planning and conflict detection.
691    ///
692    /// # Arguments
693    ///
694    /// * `base` - The base directory for installation
695    ///
696    /// # Returns
697    ///
698    /// The full path where this resource would be installed
699    ///
700    /// # Examples
701    ///
702    /// ```rust,no_run
703    /// use agpm_cli::core::Resource;
704    /// use std::path::Path;
705    ///
706    /// fn check_install_location(resource: &dyn Resource) {
707    ///     let base = Path::new("/project/resources");
708    ///     let install_path = resource.install_path(base);
709    ///     
710    ///     println!("{} would be installed to: {}",
711    ///         resource.name(),
712    ///         install_path.display()
713    ///     );
714    /// }
715    /// ```
716    fn install_path(&self, base: &Path) -> std::path::PathBuf;
717
718    /// Get structured metadata for this resource as JSON
719    ///
720    /// Returns resource metadata in a flexible JSON format that can include
721    /// version information, tags, author details, and other custom fields.
722    /// This metadata is used for resource discovery, filtering, and display.
723    ///
724    /// # Returns
725    ///
726    /// - `Ok(json_value)` containing the metadata
727    /// - `Err(error)` if metadata cannot be generated or parsed
728    ///
729    /// # Metadata Structure
730    ///
731    /// While flexible, metadata typically includes:
732    /// - `name`: Resource name
733    /// - `type`: Resource type
734    /// - `version`: Version information
735    /// - `description`: Human-readable description
736    /// - `tags`: Array of classification tags
737    /// - `dependencies`: Dependency information
738    ///
739    /// # Examples
740    ///
741    /// ```rust,no_run
742    /// use agpm_cli::core::Resource;
743    /// use anyhow::Result;
744    ///
745    /// fn show_resource_metadata(resource: &dyn Resource) -> Result<()> {
746    ///     let metadata = resource.metadata()?;
747    ///     
748    ///     if let Some(version) = metadata.get("version") {
749    ///         println!("Version: {}", version);
750    ///     }
751    ///     
752    ///     if let Some(tags) = metadata.get("tags").and_then(|t| t.as_array()) {
753    ///         print!("Tags: ");
754    ///         for tag in tags {
755    ///             print!("{} ", tag.as_str().unwrap_or("?"));
756    ///         }
757    ///         println!();
758    ///     }
759    ///     
760    ///     Ok(())
761    /// }
762    /// ```
763    fn metadata(&self) -> Result<serde_json::Value>;
764
765    /// Get a reference to this resource as [`std::any::Any`] for downcasting
766    ///
767    /// This method enables downcasting from the [`Resource`] trait object to
768    /// concrete resource implementations when needed for type-specific operations.
769    ///
770    /// # Returns
771    ///
772    /// A reference to this resource as [`std::any::Any`]
773    ///
774    /// # Examples
775    ///
776    /// ```rust,no_run
777    /// use agpm_cli::core::Resource;
778    /// use std::any::Any;
779    ///
780    /// // Hypothetical concrete resource type
781    /// struct MyAgent {
782    ///     name: String,
783    ///     // ... other fields
784    /// }
785    ///
786    /// fn try_downcast_to_agent(resource: &dyn Resource) {
787    ///     let any = resource.as_any();
788    ///     
789    ///     if let Some(agent) = any.downcast_ref::<MyAgent>() {
790    ///         println!("Successfully downcasted to MyAgent: {}", agent.name);
791    ///     } else {
792    ///         println!("Resource is not a MyAgent type");
793    ///     }
794    /// }
795    /// ```
796    fn as_any(&self) -> &dyn std::any::Any;
797}
798
799#[cfg(test)]
800mod tests {
801    use super::*;
802
803    #[test]
804    fn test_resource_type_default_directory() {
805        assert_eq!(ResourceType::Agent.default_directory(), Some(".claude/agents"));
806        assert_eq!(ResourceType::Snippet.default_directory(), Some(".claude/snippets"));
807        assert_eq!(ResourceType::Command.default_directory(), Some(".claude/commands"));
808        assert_eq!(ResourceType::McpServer.default_directory(), None);
809        assert_eq!(ResourceType::Script.default_directory(), Some(".claude/scripts"));
810        assert_eq!(ResourceType::Hook.default_directory(), None);
811        assert_eq!(ResourceType::Skill.default_directory(), Some(".claude/skills"));
812    }
813
814    #[test]
815    fn test_resource_type_display() {
816        assert_eq!(ResourceType::Agent.to_string(), "agent");
817        assert_eq!(ResourceType::Snippet.to_string(), "snippet");
818        assert_eq!(ResourceType::Command.to_string(), "command");
819        assert_eq!(ResourceType::McpServer.to_string(), "mcp-server");
820        assert_eq!(ResourceType::Script.to_string(), "script");
821        assert_eq!(ResourceType::Hook.to_string(), "hook");
822        assert_eq!(ResourceType::Skill.to_string(), "skill");
823    }
824
825    #[test]
826    fn test_resource_type_from_str() {
827        use std::str::FromStr;
828
829        assert_eq!(ResourceType::from_str("agent").unwrap(), ResourceType::Agent);
830        assert_eq!(ResourceType::from_str("snippet").unwrap(), ResourceType::Snippet);
831        assert_eq!(ResourceType::from_str("AGENT").unwrap(), ResourceType::Agent);
832        assert_eq!(ResourceType::from_str("Snippet").unwrap(), ResourceType::Snippet);
833        assert_eq!(ResourceType::from_str("command").unwrap(), ResourceType::Command);
834        assert_eq!(ResourceType::from_str("COMMAND").unwrap(), ResourceType::Command);
835        assert_eq!(ResourceType::from_str("mcp-server").unwrap(), ResourceType::McpServer);
836        assert_eq!(ResourceType::from_str("MCP").unwrap(), ResourceType::McpServer);
837        assert_eq!(ResourceType::from_str("script").unwrap(), ResourceType::Script);
838        assert_eq!(ResourceType::from_str("SCRIPT").unwrap(), ResourceType::Script);
839        assert_eq!(ResourceType::from_str("hook").unwrap(), ResourceType::Hook);
840        assert_eq!(ResourceType::from_str("HOOK").unwrap(), ResourceType::Hook);
841        assert_eq!(ResourceType::from_str("skill").unwrap(), ResourceType::Skill);
842        assert_eq!(ResourceType::from_str("SKILL").unwrap(), ResourceType::Skill);
843
844        assert!(ResourceType::from_str("invalid").is_err());
845    }
846
847    #[test]
848    fn test_resource_type_serialization() {
849        let agent = ResourceType::Agent;
850        let json = serde_json::to_string(&agent).unwrap();
851        assert_eq!(json, "\"agent\"");
852
853        let deserialized: ResourceType = serde_json::from_str(&json).unwrap();
854        assert_eq!(deserialized, ResourceType::Agent);
855
856        // Test command serialization
857        let command = ResourceType::Command;
858        let json = serde_json::to_string(&command).unwrap();
859        assert_eq!(json, "\"command\"");
860
861        let deserialized: ResourceType = serde_json::from_str(&json).unwrap();
862        assert_eq!(deserialized, ResourceType::Command);
863
864        // Test mcp-server serialization
865        let mcp_server = ResourceType::McpServer;
866        let json = serde_json::to_string(&mcp_server).unwrap();
867        assert_eq!(json, "\"mcp-server\"");
868
869        let deserialized: ResourceType = serde_json::from_str(&json).unwrap();
870        assert_eq!(deserialized, ResourceType::McpServer);
871
872        // Test script serialization
873        let script = ResourceType::Script;
874        let json = serde_json::to_string(&script).unwrap();
875        assert_eq!(json, "\"script\"");
876
877        let deserialized: ResourceType = serde_json::from_str(&json).unwrap();
878        assert_eq!(deserialized, ResourceType::Script);
879
880        // Test hook serialization
881        let hook = ResourceType::Hook;
882        let json = serde_json::to_string(&hook).unwrap();
883        assert_eq!(json, "\"hook\"");
884
885        let deserialized: ResourceType = serde_json::from_str(&json).unwrap();
886        assert_eq!(deserialized, ResourceType::Hook);
887
888        // Test skill serialization
889        let skill = ResourceType::Skill;
890        let json = serde_json::to_string(&skill).unwrap();
891        assert_eq!(json, "\"skill\"");
892
893        let deserialized: ResourceType = serde_json::from_str(&json).unwrap();
894        assert_eq!(deserialized, ResourceType::Skill);
895    }
896
897    #[test]
898    fn test_resource_type_equality() {
899        assert_eq!(ResourceType::Command, ResourceType::Command);
900        assert_ne!(ResourceType::Command, ResourceType::Agent);
901        assert_ne!(ResourceType::Command, ResourceType::Snippet);
902        assert_eq!(ResourceType::McpServer, ResourceType::McpServer);
903        assert_ne!(ResourceType::McpServer, ResourceType::Agent);
904        assert_eq!(ResourceType::Script, ResourceType::Script);
905        assert_ne!(ResourceType::Script, ResourceType::Hook);
906        assert_eq!(ResourceType::Hook, ResourceType::Hook);
907        assert_ne!(ResourceType::Hook, ResourceType::Agent);
908        assert_eq!(ResourceType::Skill, ResourceType::Skill);
909        assert_ne!(ResourceType::Skill, ResourceType::Agent);
910    }
911
912    #[test]
913    fn test_resource_type_copy() {
914        let command = ResourceType::Command;
915        let copied = command; // ResourceType implements Copy, so this creates a copy
916        assert_eq!(command, copied);
917    }
918
919    #[test]
920    fn test_resource_type_all() {
921        let all_types = ResourceType::all();
922        assert_eq!(all_types.len(), 7);
923        assert_eq!(all_types[0], ResourceType::Agent);
924        assert_eq!(all_types[1], ResourceType::Snippet);
925        assert_eq!(all_types[2], ResourceType::Command);
926        assert_eq!(all_types[3], ResourceType::McpServer);
927        assert_eq!(all_types[4], ResourceType::Script);
928        assert_eq!(all_types[5], ResourceType::Hook);
929        assert_eq!(all_types[6], ResourceType::Skill);
930
931        // Test that we can iterate
932        let mut count = 0;
933        for _ in ResourceType::all() {
934            count += 1;
935        }
936        assert_eq!(count, 7);
937    }
938
939    #[test]
940    fn test_resource_type_from_frontmatter_str() {
941        // Test singular forms
942        assert_eq!(ResourceType::from_frontmatter_str("agent"), Some(ResourceType::Agent));
943        assert_eq!(ResourceType::from_frontmatter_str("snippet"), Some(ResourceType::Snippet));
944        assert_eq!(ResourceType::from_frontmatter_str("command"), Some(ResourceType::Command));
945        assert_eq!(ResourceType::from_frontmatter_str("hook"), Some(ResourceType::Hook));
946        assert_eq!(ResourceType::from_frontmatter_str("script"), Some(ResourceType::Script));
947        assert_eq!(ResourceType::from_frontmatter_str("mcp-server"), Some(ResourceType::McpServer));
948        assert_eq!(ResourceType::from_frontmatter_str("skill"), Some(ResourceType::Skill));
949
950        // Test plural forms
951        assert_eq!(ResourceType::from_frontmatter_str("agents"), Some(ResourceType::Agent));
952        assert_eq!(ResourceType::from_frontmatter_str("snippets"), Some(ResourceType::Snippet));
953        assert_eq!(ResourceType::from_frontmatter_str("commands"), Some(ResourceType::Command));
954        assert_eq!(ResourceType::from_frontmatter_str("hooks"), Some(ResourceType::Hook));
955        assert_eq!(ResourceType::from_frontmatter_str("scripts"), Some(ResourceType::Script));
956        assert_eq!(
957            ResourceType::from_frontmatter_str("mcp-servers"),
958            Some(ResourceType::McpServer)
959        );
960        assert_eq!(ResourceType::from_frontmatter_str("skills"), Some(ResourceType::Skill));
961
962        // Test unknown types
963        assert_eq!(ResourceType::from_frontmatter_str("unknown"), None);
964        assert_eq!(ResourceType::from_frontmatter_str("invalid"), None);
965        assert_eq!(ResourceType::from_frontmatter_str(""), None);
966
967        // Test case sensitivity (should be case-sensitive as per implementation)
968        assert_eq!(ResourceType::from_frontmatter_str("Agent"), None);
969        assert_eq!(ResourceType::from_frontmatter_str("AGENTS"), None);
970        assert_eq!(ResourceType::from_frontmatter_str("Snippet"), None);
971    }
972}