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