agpm_cli/core/
mod.rs

1//! Core types and functionality for AGPM
2//!
3//! This module forms the foundation of AGPM's type system, providing fundamental abstractions
4//! for error handling, resource management, and core operations. It defines the contracts and
5//! interfaces used throughout the AGPM codebase.
6//!
7//! # Architecture Overview
8//!
9//! The core module is organized around several key concepts:
10//!
11//! ## Error Management
12//! AGPM uses a sophisticated error handling system designed for both developer ergonomics
13//! and end-user experience:
14//! - **Strongly-typed errors** ([`AgpmError`]) for precise error handling in code
15//! - **User-friendly contexts** ([`ErrorContext`]) with actionable suggestions for CLI users
16//! - **Automatic error conversion** from common standard library errors
17//! - **Contextual suggestions** tailored to specific error conditions
18//!
19//! ## Resource Abstractions
20//! Resources are the core entities managed by AGPM:
21//! - **Resource types** ([`ResourceType`]) define categories (agents, snippets)
22//! - **Resource trait** provides common interface for all resource types  
23//! - **Type detection** automatically identifies resources
24//! - **Extensible design** allows future resource types to be added easily
25//!
26//! # Modules
27//!
28//! ## `error` - Comprehensive Error Handling
29//!
30//! The error module provides:
31//! - [`AgpmError`] - Enumerated error types covering all AGPM failure modes
32//! - [`ErrorContext`] - User-friendly error wrapper with suggestions and details
33//! - [`user_friendly_error`] - Convert any error to user-friendly format
34//! - [`IntoAnyhowWithContext`] - Extension trait for error conversion
35//!
36//! ## `resource` - Resource Type System
37//!
38//! The resource module provides:
39//! - [`ResourceType`] - Enumeration of supported resource types
40//! - `Resource` - Trait interface for all resource implementations
41//! - Type detection functions - Automatic resource type detection
42//!
43//! # Design Principles
44//!
45//! ## Error First Design
46//! Every operation that can fail returns a [`Result`] type with meaningful error information.
47//! Errors are designed to be informative, actionable, and user-friendly.
48//!
49//! ## Type Safety
50//! Strong typing prevents invalid operations and catches errors at compile time.
51//! Resource types, error variants, and operation modes are all statically typed.
52//!
53//! ## Extensibility
54//! The core abstractions are designed to support future expansion without breaking changes.
55//! New resource types and error variants can be added while maintaining compatibility.
56//!
57//! ## User Experience
58//! All user-facing errors include contextual suggestions and clear guidance for resolution.
59//! Terminal colors and formatting enhance readability and highlight important information.
60//!
61//! # Examples
62//!
63//! ## Error Handling Pattern
64//!
65//! ```rust,no_run
66//! use agpm_cli::core::{AgpmError, ErrorContext, user_friendly_error};
67//! use anyhow::Result;
68//!
69//! fn example_operation() -> Result<String> {
70//!     // Simulate an operation that might fail
71//!     Err(AgpmError::ManifestNotFound.into())
72//! }
73//!
74//! fn handle_operation() {
75//!     match example_operation() {
76//!         Ok(result) => println!("Success: {}", result),
77//!         Err(e) => {
78//!             // Convert to user-friendly error and display
79//!             let friendly = user_friendly_error(e);
80//!             friendly.display(); // Shows colored error with suggestions
81//!         }
82//!     }
83//! }
84//! ```
85//!
86//! ## Resource Type Detection
87//!
88//! ```rust,no_run
89//! use agpm_cli::core::{ResourceType, detect_resource_type};
90//! use std::path::Path;
91//! use tempfile::tempdir;
92//!
93//! fn discover_resources() -> anyhow::Result<()> {
94//!     let temp_dir = tempdir()?;
95//!     let path = temp_dir.path();
96//!
97//!     // Create a resource manifest
98//!     std::fs::write(path.join("agent.toml"), "# Agent configuration")?;
99//!     
100//!     // Detect the resource type
101//!     if let Some(resource_type) = detect_resource_type(path) {
102//!         match resource_type {
103//!             ResourceType::Agent => {
104//!                 println!("Found agent resource");
105//!                 println!("Install dir: {}", resource_type.default_directory().unwrap_or("none"));
106//!             }
107//!             ResourceType::Snippet => {
108//!                 println!("Found snippet resource");
109//!                 println!("Install dir: {}", resource_type.default_directory().unwrap_or("none"));
110//!             }
111//!             ResourceType::Command => {
112//!                 println!("Found command resource");
113//!                 println!("Install dir: {}", resource_type.default_directory().unwrap_or("none"));
114//!             }
115//!             ResourceType::McpServer => {
116//!                 println!("Found MCP server configuration");
117//!                 println!("Install dir: {}", resource_type.default_directory().unwrap_or("none"));
118//!             }
119//!             ResourceType::Script => {
120//!                 println!("Found script resource");
121//!                 println!("Install dir: {}", resource_type.default_directory().unwrap_or("none"));
122//!             }
123//!             ResourceType::Hook => {
124//!                 println!("Found hook configuration");
125//!                 println!("Install dir: {}", resource_type.default_directory().unwrap_or("none"));
126//!             }
127//!         }
128//!     } else {
129//!         println!("No recognized resource found");
130//!     }
131//!     
132//!     Ok(())
133//! }
134//! ```
135//!
136//! ## Resource Trait Usage
137//!
138//! ```rust,no_run
139//! use agpm_cli::core::{Resource, ResourceType};
140//! use anyhow::Result;
141//! use std::path::Path;
142//!
143//! fn process_any_resource(resource: &dyn Resource) -> Result<()> {
144//!     // Get basic information
145//!     println!("Processing: {} ({})", resource.name(), resource.resource_type());
146//!     
147//!     if let Some(desc) = resource.description() {
148//!         println!("Description: {}", desc);
149//!     }
150//!     
151//!     // Validate before processing
152//!     resource.validate()?;
153//!     
154//!     // Get metadata for analysis
155//!     let metadata = resource.metadata()?;
156//!     if let Some(version) = metadata.get("version") {
157//!         println!("Version: {}", version);
158//!     }
159//!     
160//!     // Install to target location
161//!     let target = Path::new("./resources");
162//!     let install_path = resource.install_path(target);
163//!     println!("Would install to: {}", install_path.display());
164//!     
165//!     Ok(())
166//! }
167//! ```
168//!
169//! ## Error Context Creation
170//!
171//! ```rust,no_run
172//! use agpm_cli::core::{AgpmError, ErrorContext};
173//!
174//! fn create_helpful_error() -> ErrorContext {
175//!     ErrorContext::new(AgpmError::GitNotFound)
176//!         .with_suggestion("Install git from https://git-scm.com/ or use your package manager")
177//!         .with_details("AGPM requires git to clone and manage source repositories")
178//! }
179//!
180//! fn display_error() {
181//!     let error = create_helpful_error();
182//!     error.display(); // Shows colored output with error, details, and suggestion
183//! }
184//! ```
185//!
186//! # Integration with Other Modules
187//!
188//! The core module provides types used throughout AGPM:
189//! - **CLI commands** use [`AgpmError`] and [`ErrorContext`] for user feedback
190//! - **Git operations** return [`AgpmError`] variants for specific failure modes
191//! - **Manifest parsing** uses [`Resource`] trait for type-agnostic operations
192//! - **Installation** relies on [`ResourceType`] for path generation and validation
193//! - **Dependency resolution** uses error types for constraint violations
194//!
195//! # Thread Safety
196//!
197//! All core types are designed to be thread-safe where appropriate:
198//! - [`ResourceType`] is [`Copy`] and can be shared freely
199//! - [`AgpmError`] implements [`Clone`] for error propagation
200//! - [`Resource`] trait is object-safe for dynamic dispatch
201//!
202//! [`Result`]: std::result::Result
203//! [`Copy`]: std::marker::Copy  
204//! [`Clone`]: std::clone::Clone
205
206pub mod error;
207pub mod error_builders;
208pub mod error_helpers;
209mod resource;
210pub mod resource_iterator;
211
212pub use error::{AgpmError, ErrorContext, IntoAnyhowWithContext, user_friendly_error};
213pub use error_builders::{
214    ErrorContextExt, file_error_context, git_error_context, manifest_error_context,
215};
216pub use error_helpers::{
217    FileOperations, FileOps, JsonOperations, JsonOps, LockfileOperations, LockfileOps,
218    ManifestOperations, ManifestOps, MarkdownOperations, MarkdownOps,
219};
220pub use resource::{Resource, ResourceType};
221pub use resource_iterator::{ResourceIterator, ResourceTypeExt};
222
223use std::path::Path;
224
225/// Detect the resource type in a directory by examining manifest files
226///
227/// This function provides automatic resource type detection based on manifest file presence.
228///
229/// # Arguments
230///
231/// * `path` - The directory path to examine for resource manifests
232///
233/// # Returns
234///
235/// - `Some(ResourceType::Agent)` if `agent.toml` exists
236/// - `Some(ResourceType::Snippet)` if `snippet.toml` exists (and no `agent.toml`)
237/// - `None` if no recognized manifest files are found
238///
239/// # Examples
240///
241/// ```rust,no_run
242/// use agpm_cli::core::{ResourceType, detect_resource_type};
243/// use tempfile::tempdir;
244/// use std::fs;
245///
246/// let temp = tempdir().unwrap();
247/// let path = temp.path();
248///
249/// // No manifest initially
250/// assert_eq!(detect_resource_type(path), None);
251///
252/// // Add agent manifest
253/// fs::write(path.join("agent.toml"), "# Agent config").unwrap();
254/// assert_eq!(detect_resource_type(path), Some(ResourceType::Agent));
255/// ```
256#[must_use]
257pub fn detect_resource_type(path: &Path) -> Option<ResourceType> {
258    if path.join("agent.toml").exists() {
259        Some(ResourceType::Agent)
260    } else if path.join("snippet.toml").exists() {
261        Some(ResourceType::Snippet)
262    } else {
263        None
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use std::fs;
271    use tempfile::TempDir;
272
273    #[test]
274    fn test_detect_resource_type_agent() {
275        let temp_dir = TempDir::new().unwrap();
276        let path = temp_dir.path();
277
278        // Create agent.toml
279        fs::write(path.join("agent.toml"), "").unwrap();
280
281        assert_eq!(detect_resource_type(path), Some(ResourceType::Agent));
282    }
283
284    #[test]
285    fn test_detect_resource_type_snippet() {
286        let temp_dir = TempDir::new().unwrap();
287        let path = temp_dir.path();
288
289        // Create snippet.toml
290        fs::write(path.join("snippet.toml"), "").unwrap();
291
292        assert_eq!(detect_resource_type(path), Some(ResourceType::Snippet));
293    }
294
295    #[test]
296    fn test_detect_resource_type_none() {
297        let temp_dir = TempDir::new().unwrap();
298        let path = temp_dir.path();
299
300        // No resource files
301        assert_eq!(detect_resource_type(path), None);
302    }
303
304    #[test]
305    fn test_detect_resource_type_prefers_agent() {
306        let temp_dir = TempDir::new().unwrap();
307        let path = temp_dir.path();
308
309        // Create both files - agent should take precedence
310        fs::write(path.join("agent.toml"), "").unwrap();
311        fs::write(path.join("snippet.toml"), "").unwrap();
312
313        assert_eq!(detect_resource_type(path), Some(ResourceType::Agent));
314    }
315}