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}