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//!             ResourceType::Skill => {
141//!                 println!("Found skill resource");
142//!                 println!("Install dir: {}", resource_type.default_directory().unwrap_or("none"));
143//!             }
144//!         }
145//!     } else {
146//!         println!("No recognized resource found");
147//!     }
148//!     
149//!     Ok(())
150//! }
151//! ```
152//!
153//! ## Resource Trait Usage
154//!
155//! ```rust,no_run
156//! use agpm_cli::core::{Resource, ResourceType};
157//! use anyhow::Result;
158//! use std::path::Path;
159//!
160//! fn process_any_resource(resource: &dyn Resource) -> Result<()> {
161//!     // Get basic information
162//!     println!("Processing: {} ({})", resource.name(), resource.resource_type());
163//!     
164//!     if let Some(desc) = resource.description() {
165//!         println!("Description: {}", desc);
166//!     }
167//!     
168//!     // Validate before processing
169//!     resource.validate()?;
170//!     
171//!     // Get metadata for analysis
172//!     let metadata = resource.metadata()?;
173//!     if let Some(version) = metadata.get("version") {
174//!         println!("Version: {}", version);
175//!     }
176//!     
177//!     // Install to target location
178//!     let target = Path::new("./resources");
179//!     let install_path = resource.install_path(target);
180//!     println!("Would install to: {}", install_path.display());
181//!     
182//!     Ok(())
183//! }
184//! ```
185//!
186//! ## Error Context Creation
187//!
188//! ```rust,no_run
189//! use agpm_cli::core::{AgpmError, ErrorContext};
190//!
191//! fn create_helpful_error() -> ErrorContext {
192//!     ErrorContext::new(AgpmError::GitNotFound)
193//!         .with_suggestion("Install git from https://git-scm.com/ or use your package manager")
194//!         .with_details("AGPM requires git to clone and manage source repositories")
195//! }
196//!
197//! fn display_error() {
198//!     let error = create_helpful_error();
199//!     error.display(); // Shows colored output with error, details, and suggestion
200//! }
201//! ```
202//!
203//! # Integration with Other Modules
204//!
205//! The core module provides types used throughout AGPM:
206//! - **CLI commands** use [`AgpmError`] and [`ErrorContext`] for user feedback
207//! - **Git operations** return [`AgpmError`] variants for specific failure modes
208//! - **Manifest parsing** uses [`Resource`] trait for type-agnostic operations
209//! - **Installation** relies on [`ResourceType`] for path generation and validation
210//! - **Dependency resolution** uses error types for constraint violations
211//!
212//! # Thread Safety
213//!
214//! All core types are designed to be thread-safe where appropriate:
215//! - [`ResourceType`] is [`Copy`] and can be shared freely
216//! - [`AgpmError`] implements [`Clone`] for error propagation
217//! - [`Resource`] trait is object-safe for dynamic dispatch
218//!
219//! [`Result`]: std::result::Result
220//! [`Copy`]: std::marker::Copy  
221//! [`Clone`]: std::clone::Clone
222
223pub mod error;
224pub mod error_builders;
225pub mod error_formatting;
226pub mod error_helpers;
227pub mod file_error;
228pub mod operation_context;
229mod resource;
230pub mod resource_iterator;
231
232pub use error::{AgpmError, ErrorContext, IntoAnyhowWithContext};
233pub use error_builders::{
234    ErrorContextExt, file_error_context, git_error_context, manifest_error_context,
235};
236pub use error_formatting::user_friendly_error;
237pub use error_helpers::{
238    FileOperations, FileOps, JsonOperations, JsonOps, LockfileOperations, LockfileOps,
239    ManifestOperations, ManifestOps, MarkdownOperations, MarkdownOps,
240};
241pub use operation_context::OperationContext;
242pub use resource::{Resource, ResourceType};
243pub use resource_iterator::{ResourceIterator, ResourceTypeExt};
244
245use std::path::Path;
246
247/// Detect the resource type in a directory by examining manifest files
248///
249/// This function provides automatic resource type detection based on manifest file presence.
250///
251/// # Arguments
252///
253/// * `path` - The directory path to examine for resource manifests
254///
255/// # Returns
256///
257/// - `Some(ResourceType::Agent)` if `agent.toml` exists
258/// - `Some(ResourceType::Snippet)` if `snippet.toml` exists (and no `agent.toml`)
259/// - `None` if no recognized manifest files are found
260///
261/// # Examples
262///
263/// ```rust,no_run
264/// use agpm_cli::core::{ResourceType, detect_resource_type};
265/// use tempfile::tempdir;
266/// use std::fs;
267///
268/// let temp = tempdir().unwrap();
269/// let path = temp.path();
270///
271/// // No manifest initially
272/// assert_eq!(detect_resource_type(path), None);
273///
274/// // Add agent manifest
275/// fs::write(path.join("agent.toml"), "# Agent config").unwrap();
276/// assert_eq!(detect_resource_type(path), Some(ResourceType::Agent));
277/// ```
278#[must_use]
279pub fn detect_resource_type(path: &Path) -> Option<ResourceType> {
280    if path.join("agent.toml").exists() {
281        Some(ResourceType::Agent)
282    } else if path.join("snippet.toml").exists() {
283        Some(ResourceType::Snippet)
284    } else {
285        None
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292    use std::fs;
293    use tempfile::TempDir;
294
295    #[test]
296    fn test_detect_resource_type_agent() {
297        let temp_dir = TempDir::new().unwrap();
298        let path = temp_dir.path();
299
300        // Create agent.toml
301        fs::write(path.join("agent.toml"), "").unwrap();
302
303        assert_eq!(detect_resource_type(path), Some(ResourceType::Agent));
304    }
305
306    #[test]
307    fn test_detect_resource_type_snippet() {
308        let temp_dir = TempDir::new().unwrap();
309        let path = temp_dir.path();
310
311        // Create snippet.toml
312        fs::write(path.join("snippet.toml"), "").unwrap();
313
314        assert_eq!(detect_resource_type(path), Some(ResourceType::Snippet));
315    }
316
317    #[test]
318    fn test_detect_resource_type_none() {
319        let temp_dir = TempDir::new().unwrap();
320        let path = temp_dir.path();
321
322        // No resource files
323        assert_eq!(detect_resource_type(path), None);
324    }
325
326    #[test]
327    fn test_detect_resource_type_prefers_agent() {
328        let temp_dir = TempDir::new().unwrap();
329        let path = temp_dir.path();
330
331        // Create both files - agent should take precedence
332        fs::write(path.join("agent.toml"), "").unwrap();
333        fs::write(path.join("snippet.toml"), "").unwrap();
334
335        assert_eq!(detect_resource_type(path), Some(ResourceType::Agent));
336    }
337}