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(), ".claude/agents");
43//! assert_eq!(snippet_type.default_directory(), ".claude/agpm/snippets");
44//! assert_eq!(script_type.default_directory(), ".claude/agpm/scripts");
45//! assert_eq!(hook_type.default_directory(), ".claude/agpm/hooks");
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/agpm/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/agpm/scripts`
93/// - **Common Use Cases**: Validation scripts, automation tools, hook executables
94///
95/// ## Hook
96/// - **Purpose**: Event-based automation configurations for Claude Code
97/// - **Default Directory**: `.claude/agpm/hooks`
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(), ".claude/agents");
137///
138/// let snippet = ResourceType::Snippet;  
139/// assert_eq!(snippet.default_directory(), ".claude/agpm/snippets");
140///
141/// let script = ResourceType::Script;
142/// assert_eq!(script.default_directory(), ".claude/agpm/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/agpm/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`] → `".claude/agents"`
261    /// - [`Snippet`] → `".claude/agpm/snippets"`
262    /// - [`Command`] → `.claude/commands`
263    /// - [`McpServer`] → `.claude/agpm/mcp-servers`
264    /// - [`Script`] → `.claude/agpm/scripts`
265    /// - [`Hook`] → `.claude/agpm/hooks`
266    ///
267    /// # Examples
268    ///
269    /// ```rust,no_run
270    /// use agpm_cli::core::ResourceType;
271    ///
272    /// assert_eq!(ResourceType::Agent.default_directory(), ".claude/agents");
273    /// assert_eq!(ResourceType::Snippet.default_directory(), ".claude/agpm/snippets");
274    /// assert_eq!(ResourceType::Command.default_directory(), ".claude/commands");
275    /// assert_eq!(ResourceType::McpServer.default_directory(), ".claude/agpm/mcp-servers");
276    /// assert_eq!(ResourceType::Script.default_directory(), ".claude/agpm/scripts");
277    /// assert_eq!(ResourceType::Hook.default_directory(), ".claude/agpm/hooks");
278    /// ```
279    ///
280    /// # Note
281    ///
282    /// This is just the default convention. Users can install resources to any
283    /// directory by specifying custom paths in their manifest files.
284    ///
285    /// [`Agent`]: ResourceType::Agent
286    /// [`Snippet`]: ResourceType::Snippet
287    /// [`Command`]: ResourceType::Command
288    /// [`McpServer`]: ResourceType::McpServer
289    /// [`Script`]: ResourceType::Script
290    /// [`Hook`]: ResourceType::Hook
291    #[must_use]
292    pub const fn default_directory(&self) -> &str {
293        match self {
294            Self::Agent => ".claude/agents",
295            Self::Snippet => ".claude/agpm/snippets",
296            Self::Command => ".claude/commands",
297            Self::McpServer => ".claude/agpm/mcp-servers",
298            Self::Script => ".claude/agpm/scripts",
299            Self::Hook => ".claude/agpm/hooks",
300        }
301    }
302}
303
304impl std::fmt::Display for ResourceType {
305    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306        match self {
307            Self::Agent => write!(f, "agent"),
308            Self::Snippet => write!(f, "snippet"),
309            Self::Command => write!(f, "command"),
310            Self::McpServer => write!(f, "mcp-server"),
311            Self::Script => write!(f, "script"),
312            Self::Hook => write!(f, "hook"),
313        }
314    }
315}
316
317impl std::str::FromStr for ResourceType {
318    type Err = crate::core::AgpmError;
319
320    fn from_str(s: &str) -> Result<Self, Self::Err> {
321        match s.to_lowercase().as_str() {
322            "agent" | "agents" => Ok(Self::Agent),
323            "snippet" | "snippets" => Ok(Self::Snippet),
324            "command" | "commands" => Ok(Self::Command),
325            "mcp-server" | "mcp-servers" | "mcpserver" | "mcp" => Ok(Self::McpServer),
326            "script" | "scripts" => Ok(Self::Script),
327            "hook" | "hooks" => Ok(Self::Hook),
328            _ => Err(crate::core::AgpmError::InvalidResourceType {
329                resource_type: s.to_string(),
330            }),
331        }
332    }
333}
334
335/// Base trait defining the interface for all AGPM resources
336///
337/// This trait provides a common interface for different types of resources (agents, snippets)
338/// managed by AGPM. It abstracts the core operations that can be performed on any resource,
339/// including validation, installation, and metadata access.
340///
341/// # Design Principles
342///
343/// - **Type Safety**: Each resource has a specific [`ResourceType`]
344/// - **Validation**: Resources can validate their own structure and dependencies
345/// - **Installation**: Resources know how to install themselves to target locations
346/// - **Metadata**: Resources provide structured metadata for tooling and display
347/// - **Flexibility**: Resources can be profiled or configured during installation
348///
349/// # Implementation Requirements
350///
351/// Implementors of this trait should:
352/// - Provide meaningful error messages in validation failures
353/// - Support atomic installation operations (no partial installs on failure)
354/// - Generate deterministic installation paths
355/// - Include rich metadata for resource discovery and management
356///
357/// # Examples
358///
359/// ## Basic Resource Usage Pattern
360///
361/// ```rust,no_run
362/// use agpm_cli::core::{Resource, ResourceType};
363/// use anyhow::Result;
364/// use std::path::Path;
365///
366/// fn process_resource(resource: &dyn Resource) -> Result<()> {
367///     // Get basic information
368///     println!("Processing resource: {}", resource.name());
369///     println!("Type: {}", resource.resource_type());
370///     
371///     if let Some(description) = resource.description() {
372///         println!("Description: {}", description);
373///     }
374///     
375///     // Validate the resource
376///     resource.validate()?;
377///     
378///     // Install to default location
379///     let target = Path::new("./resources");
380///     let install_path = resource.install_path(target);
381///     resource.install(&install_path, None)?;
382///     
383///     Ok(())
384/// }
385/// ```
386///
387/// ## Metadata Extraction
388///
389/// ```rust,no_run
390/// use agpm_cli::core::Resource;
391/// use anyhow::Result;
392///
393/// fn extract_metadata(resource: &dyn Resource) -> Result<()> {
394///     let metadata = resource.metadata()?;
395///     
396///     // Metadata is JSON Value for flexibility
397///     if let Some(version) = metadata.get("version") {
398///         println!("Resource version: {}", version);
399///     }
400///     
401///     if let Some(tags) = metadata.get("tags").and_then(|t| t.as_array()) {
402///         println!("Tags: {:?}", tags);
403///     }
404///     
405///     Ok(())
406/// }
407/// ```
408///
409/// # Trait Object Usage
410///
411/// The trait is object-safe and can be used as a trait object:
412///
413/// ```rust,no_run
414/// use agpm_cli::core::Resource;
415/// use std::any::Any;
416///
417/// fn handle_resource(resource: Box<dyn Resource>) {
418///     println!("Handling resource: {}", resource.name());
419///     
420///     // Can be downcasted to concrete types if needed
421///     let any = resource.as_any();
422///     // ... downcasting logic
423/// }
424/// ```
425pub trait Resource {
426    /// Get the unique name identifier for this resource
427    ///
428    /// The name is used to identify the resource in manifests, lockfiles,
429    /// and CLI operations. It should be unique within a project's namespace.
430    ///
431    /// # Returns
432    ///
433    /// A string slice containing the resource name
434    ///
435    /// # Examples
436    ///
437    /// ```rust,no_run
438    /// use agpm_cli::core::Resource;
439    ///
440    /// fn print_resource_info(resource: &dyn Resource) {
441    ///     println!("Resource name: {}", resource.name());
442    /// }
443    /// ```
444    fn name(&self) -> &str;
445
446    /// Get the resource type classification
447    ///
448    /// Returns the [`ResourceType`] enum value that identifies what kind of
449    /// resource this is (Agent, Snippet, etc.).
450    ///
451    /// # Returns
452    ///
453    /// The [`ResourceType`] for this resource
454    ///
455    /// # Examples
456    ///
457    /// ```rust,no_run
458    /// use agpm_cli::core::{Resource, ResourceType};
459    ///
460    /// fn categorize_resource(resource: &dyn Resource) {
461    ///     match resource.resource_type() {
462    ///         ResourceType::Agent => println!("This is an AI agent"),
463    ///         ResourceType::Snippet => println!("This is a code snippet"),
464    ///         ResourceType::Command => println!("This is a Claude Code command"),
465    ///         ResourceType::McpServer => println!("This is an MCP server"),
466    ///         ResourceType::Script => println!("This is an executable script"),
467    ///         ResourceType::Hook => println!("This is a hook configuration"),
468    ///     }
469    /// }
470    /// ```
471    fn resource_type(&self) -> ResourceType;
472
473    /// Get the human-readable description of this resource
474    ///
475    /// Returns an optional description that explains what the resource does
476    /// or how it should be used. This is typically displayed in resource
477    /// listings and help text.
478    ///
479    /// # Returns
480    ///
481    /// - `Some(description)` if the resource has a description
482    /// - `None` if no description is available
483    ///
484    /// # Examples
485    ///
486    /// ```rust,no_run
487    /// use agpm_cli::core::Resource;
488    ///
489    /// fn show_resource_details(resource: &dyn Resource) {
490    ///     println!("Name: {}", resource.name());
491    ///     if let Some(desc) = resource.description() {
492    ///         println!("Description: {}", desc);
493    ///     } else {
494    ///         println!("No description available");
495    ///     }
496    /// }
497    /// ```
498    fn description(&self) -> Option<&str>;
499
500    /// Get the list of dependencies required by this resource
501    ///
502    /// Returns dependencies that must be installed before this resource
503    /// can function properly. Dependencies are resolved automatically
504    /// during installation.
505    ///
506    /// # Returns
507    ///
508    /// - `Some(dependencies)` if the resource has dependencies
509    /// - `None` if the resource has no dependencies
510    ///
511    /// # Examples
512    ///
513    /// ```rust,no_run
514    /// use agpm_cli::core::Resource;
515    ///
516    /// fn check_dependencies(resource: &dyn Resource) {
517    ///     if let Some(deps) = resource.dependencies() {
518    ///         println!("{} has {} dependencies", resource.name(), deps.len());
519    ///         for dep in deps {
520    ///             // Process dependency...
521    ///         }
522    ///     } else {
523    ///         println!("{} has no dependencies", resource.name());
524    ///     }
525    /// }
526    /// ```
527    fn dependencies(&self) -> Option<&[crate::config::Dependency]>;
528
529    /// Validate the resource structure and content
530    ///
531    /// Performs comprehensive validation of the resource including:
532    /// - File structure integrity
533    /// - Content format validation
534    /// - Dependency constraint checking
535    /// - Metadata consistency
536    ///
537    /// # Returns
538    ///
539    /// - `Ok(())` if the resource is valid
540    /// - `Err(error)` with detailed validation failure information
541    ///
542    /// # Examples
543    ///
544    /// ```rust,no_run
545    /// use agpm_cli::core::Resource;
546    /// use anyhow::Result;
547    ///
548    /// fn validate_before_install(resource: &dyn Resource) -> Result<()> {
549    ///     resource.validate()
550    ///         .map_err(|e| anyhow::anyhow!("Resource validation failed: {}", e))?;
551    ///     
552    ///     println!("Resource {} is valid", resource.name());
553    ///     Ok(())
554    /// }
555    /// ```
556    fn validate(&self) -> Result<()>;
557
558    /// Install the resource to the specified target path
559    ///
560    /// Performs the actual installation of the resource files to the target
561    /// location. This operation should be atomic - either it succeeds completely
562    /// or fails without making any changes.
563    ///
564    /// # Arguments
565    ///
566    /// * `target` - The directory path where the resource should be installed
567    /// * `profile` - Optional profile name for customized installation (may be unused)
568    ///
569    /// # Returns
570    ///
571    /// - `Ok(())` if installation succeeds
572    /// - `Err(error)` if installation fails with detailed error information
573    ///
574    /// # Examples
575    ///
576    /// ```rust,no_run
577    /// use agpm_cli::core::Resource;
578    /// use std::path::Path;
579    /// use anyhow::Result;
580    ///
581    /// fn install_resource(resource: &dyn Resource) -> Result<()> {
582    ///     let target = Path::new("./installed-resources");
583    ///     
584    ///     // Validate first
585    ///     resource.validate()?;
586    ///     
587    ///     // Install without profile
588    ///     resource.install(target, None)?;
589    ///     
590    ///     println!("Successfully installed {}", resource.name());
591    ///     Ok(())
592    /// }
593    /// ```
594    fn install(&self, target: &Path, profile: Option<&str>) -> Result<()>;
595
596    /// Calculate the installation path for this resource
597    ///
598    /// Determines where this resource would be installed relative to a base
599    /// directory. This is used for path planning and conflict detection.
600    ///
601    /// # Arguments
602    ///
603    /// * `base` - The base directory for installation
604    ///
605    /// # Returns
606    ///
607    /// The full path where this resource would be installed
608    ///
609    /// # Examples
610    ///
611    /// ```rust,no_run
612    /// use agpm_cli::core::Resource;
613    /// use std::path::Path;
614    ///
615    /// fn check_install_location(resource: &dyn Resource) {
616    ///     let base = Path::new("/project/resources");
617    ///     let install_path = resource.install_path(base);
618    ///     
619    ///     println!("{} would be installed to: {}",
620    ///         resource.name(),
621    ///         install_path.display()
622    ///     );
623    /// }
624    /// ```
625    fn install_path(&self, base: &Path) -> std::path::PathBuf;
626
627    /// Get structured metadata for this resource as JSON
628    ///
629    /// Returns resource metadata in a flexible JSON format that can include
630    /// version information, tags, author details, and other custom fields.
631    /// This metadata is used for resource discovery, filtering, and display.
632    ///
633    /// # Returns
634    ///
635    /// - `Ok(json_value)` containing the metadata
636    /// - `Err(error)` if metadata cannot be generated or parsed
637    ///
638    /// # Metadata Structure
639    ///
640    /// While flexible, metadata typically includes:
641    /// - `name`: Resource name
642    /// - `type`: Resource type
643    /// - `version`: Version information
644    /// - `description`: Human-readable description
645    /// - `tags`: Array of classification tags
646    /// - `dependencies`: Dependency information
647    ///
648    /// # Examples
649    ///
650    /// ```rust,no_run
651    /// use agpm_cli::core::Resource;
652    /// use anyhow::Result;
653    ///
654    /// fn show_resource_metadata(resource: &dyn Resource) -> Result<()> {
655    ///     let metadata = resource.metadata()?;
656    ///     
657    ///     if let Some(version) = metadata.get("version") {
658    ///         println!("Version: {}", version);
659    ///     }
660    ///     
661    ///     if let Some(tags) = metadata.get("tags").and_then(|t| t.as_array()) {
662    ///         print!("Tags: ");
663    ///         for tag in tags {
664    ///             print!("{} ", tag.as_str().unwrap_or("?"));
665    ///         }
666    ///         println!();
667    ///     }
668    ///     
669    ///     Ok(())
670    /// }
671    /// ```
672    fn metadata(&self) -> Result<serde_json::Value>;
673
674    /// Get a reference to this resource as [`std::any::Any`] for downcasting
675    ///
676    /// This method enables downcasting from the [`Resource`] trait object to
677    /// concrete resource implementations when needed for type-specific operations.
678    ///
679    /// # Returns
680    ///
681    /// A reference to this resource as [`std::any::Any`]
682    ///
683    /// # Examples
684    ///
685    /// ```rust,no_run
686    /// use agpm_cli::core::Resource;
687    /// use std::any::Any;
688    ///
689    /// // Hypothetical concrete resource type
690    /// struct MyAgent {
691    ///     name: String,
692    ///     // ... other fields
693    /// }
694    ///
695    /// fn try_downcast_to_agent(resource: &dyn Resource) {
696    ///     let any = resource.as_any();
697    ///     
698    ///     if let Some(agent) = any.downcast_ref::<MyAgent>() {
699    ///         println!("Successfully downcasted to MyAgent: {}", agent.name);
700    ///     } else {
701    ///         println!("Resource is not a MyAgent type");
702    ///     }
703    /// }
704    /// ```
705    fn as_any(&self) -> &dyn std::any::Any;
706}
707
708#[cfg(test)]
709mod tests {
710    use super::*;
711
712    #[test]
713    fn test_resource_type_default_directory() {
714        assert_eq!(ResourceType::Agent.default_directory(), ".claude/agents");
715        assert_eq!(ResourceType::Snippet.default_directory(), ".claude/agpm/snippets");
716        assert_eq!(ResourceType::Command.default_directory(), ".claude/commands");
717        assert_eq!(ResourceType::McpServer.default_directory(), ".claude/agpm/mcp-servers");
718        assert_eq!(ResourceType::Script.default_directory(), ".claude/agpm/scripts");
719        assert_eq!(ResourceType::Hook.default_directory(), ".claude/agpm/hooks");
720    }
721
722    #[test]
723    fn test_resource_type_display() {
724        assert_eq!(ResourceType::Agent.to_string(), "agent");
725        assert_eq!(ResourceType::Snippet.to_string(), "snippet");
726        assert_eq!(ResourceType::Command.to_string(), "command");
727        assert_eq!(ResourceType::McpServer.to_string(), "mcp-server");
728        assert_eq!(ResourceType::Script.to_string(), "script");
729        assert_eq!(ResourceType::Hook.to_string(), "hook");
730    }
731
732    #[test]
733    fn test_resource_type_from_str() {
734        use std::str::FromStr;
735
736        assert_eq!(ResourceType::from_str("agent").unwrap(), ResourceType::Agent);
737        assert_eq!(ResourceType::from_str("snippet").unwrap(), ResourceType::Snippet);
738        assert_eq!(ResourceType::from_str("AGENT").unwrap(), ResourceType::Agent);
739        assert_eq!(ResourceType::from_str("Snippet").unwrap(), ResourceType::Snippet);
740        assert_eq!(ResourceType::from_str("command").unwrap(), ResourceType::Command);
741        assert_eq!(ResourceType::from_str("COMMAND").unwrap(), ResourceType::Command);
742        assert_eq!(ResourceType::from_str("mcp-server").unwrap(), ResourceType::McpServer);
743        assert_eq!(ResourceType::from_str("MCP").unwrap(), ResourceType::McpServer);
744        assert_eq!(ResourceType::from_str("script").unwrap(), ResourceType::Script);
745        assert_eq!(ResourceType::from_str("SCRIPT").unwrap(), ResourceType::Script);
746        assert_eq!(ResourceType::from_str("hook").unwrap(), ResourceType::Hook);
747        assert_eq!(ResourceType::from_str("HOOK").unwrap(), ResourceType::Hook);
748
749        assert!(ResourceType::from_str("invalid").is_err());
750    }
751
752    #[test]
753    fn test_resource_type_serialization() {
754        let agent = ResourceType::Agent;
755        let json = serde_json::to_string(&agent).unwrap();
756        assert_eq!(json, "\"agent\"");
757
758        let deserialized: ResourceType = serde_json::from_str(&json).unwrap();
759        assert_eq!(deserialized, ResourceType::Agent);
760
761        // Test command serialization
762        let command = ResourceType::Command;
763        let json = serde_json::to_string(&command).unwrap();
764        assert_eq!(json, "\"command\"");
765
766        let deserialized: ResourceType = serde_json::from_str(&json).unwrap();
767        assert_eq!(deserialized, ResourceType::Command);
768
769        // Test mcp-server serialization
770        let mcp_server = ResourceType::McpServer;
771        let json = serde_json::to_string(&mcp_server).unwrap();
772        assert_eq!(json, "\"mcp-server\"");
773
774        let deserialized: ResourceType = serde_json::from_str(&json).unwrap();
775        assert_eq!(deserialized, ResourceType::McpServer);
776
777        // Test script serialization
778        let script = ResourceType::Script;
779        let json = serde_json::to_string(&script).unwrap();
780        assert_eq!(json, "\"script\"");
781
782        let deserialized: ResourceType = serde_json::from_str(&json).unwrap();
783        assert_eq!(deserialized, ResourceType::Script);
784
785        // Test hook serialization
786        let hook = ResourceType::Hook;
787        let json = serde_json::to_string(&hook).unwrap();
788        assert_eq!(json, "\"hook\"");
789
790        let deserialized: ResourceType = serde_json::from_str(&json).unwrap();
791        assert_eq!(deserialized, ResourceType::Hook);
792    }
793
794    #[test]
795    fn test_resource_type_equality() {
796        assert_eq!(ResourceType::Command, ResourceType::Command);
797        assert_ne!(ResourceType::Command, ResourceType::Agent);
798        assert_ne!(ResourceType::Command, ResourceType::Snippet);
799        assert_eq!(ResourceType::McpServer, ResourceType::McpServer);
800        assert_ne!(ResourceType::McpServer, ResourceType::Agent);
801        assert_eq!(ResourceType::Script, ResourceType::Script);
802        assert_ne!(ResourceType::Script, ResourceType::Hook);
803        assert_eq!(ResourceType::Hook, ResourceType::Hook);
804        assert_ne!(ResourceType::Hook, ResourceType::Agent);
805    }
806
807    #[test]
808    fn test_resource_type_copy() {
809        let command = ResourceType::Command;
810        let copied = command; // ResourceType implements Copy, so this creates a copy
811        assert_eq!(command, copied);
812    }
813
814    #[test]
815    fn test_resource_type_all() {
816        let all_types = ResourceType::all();
817        assert_eq!(all_types.len(), 6);
818        assert_eq!(all_types[0], ResourceType::Agent);
819        assert_eq!(all_types[1], ResourceType::Snippet);
820        assert_eq!(all_types[2], ResourceType::Command);
821        assert_eq!(all_types[3], ResourceType::McpServer);
822        assert_eq!(all_types[4], ResourceType::Script);
823        assert_eq!(all_types[5], ResourceType::Hook);
824
825        // Test that we can iterate
826        let mut count = 0;
827        for _ in ResourceType::all() {
828            count += 1;
829        }
830        assert_eq!(count, 6);
831    }
832}