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