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_formatting;
222pub mod error_helpers;
223pub mod file_error;
224pub mod operation_context;
225mod resource;
226pub mod resource_iterator;
227
228pub use error::{AgpmError, ErrorContext, IntoAnyhowWithContext};
229pub use error_builders::{
230 ErrorContextExt, file_error_context, git_error_context, manifest_error_context,
231};
232pub use error_formatting::user_friendly_error;
233pub use error_helpers::{
234 FileOperations, FileOps, JsonOperations, JsonOps, LockfileOperations, LockfileOps,
235 ManifestOperations, ManifestOps, MarkdownOperations, MarkdownOps,
236};
237pub use operation_context::OperationContext;
238pub use resource::{Resource, ResourceType};
239pub use resource_iterator::{ResourceIterator, ResourceTypeExt};
240
241use std::path::Path;
242
243/// Detect the resource type in a directory by examining manifest files
244///
245/// This function provides automatic resource type detection based on manifest file presence.
246///
247/// # Arguments
248///
249/// * `path` - The directory path to examine for resource manifests
250///
251/// # Returns
252///
253/// - `Some(ResourceType::Agent)` if `agent.toml` exists
254/// - `Some(ResourceType::Snippet)` if `snippet.toml` exists (and no `agent.toml`)
255/// - `None` if no recognized manifest files are found
256///
257/// # Examples
258///
259/// ```rust,no_run
260/// use agpm_cli::core::{ResourceType, detect_resource_type};
261/// use tempfile::tempdir;
262/// use std::fs;
263///
264/// let temp = tempdir().unwrap();
265/// let path = temp.path();
266///
267/// // No manifest initially
268/// assert_eq!(detect_resource_type(path), None);
269///
270/// // Add agent manifest
271/// fs::write(path.join("agent.toml"), "# Agent config").unwrap();
272/// assert_eq!(detect_resource_type(path), Some(ResourceType::Agent));
273/// ```
274#[must_use]
275pub fn detect_resource_type(path: &Path) -> Option<ResourceType> {
276 if path.join("agent.toml").exists() {
277 Some(ResourceType::Agent)
278 } else if path.join("snippet.toml").exists() {
279 Some(ResourceType::Snippet)
280 } else {
281 None
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288 use std::fs;
289 use tempfile::TempDir;
290
291 #[test]
292 fn test_detect_resource_type_agent() {
293 let temp_dir = TempDir::new().unwrap();
294 let path = temp_dir.path();
295
296 // Create agent.toml
297 fs::write(path.join("agent.toml"), "").unwrap();
298
299 assert_eq!(detect_resource_type(path), Some(ResourceType::Agent));
300 }
301
302 #[test]
303 fn test_detect_resource_type_snippet() {
304 let temp_dir = TempDir::new().unwrap();
305 let path = temp_dir.path();
306
307 // Create snippet.toml
308 fs::write(path.join("snippet.toml"), "").unwrap();
309
310 assert_eq!(detect_resource_type(path), Some(ResourceType::Snippet));
311 }
312
313 #[test]
314 fn test_detect_resource_type_none() {
315 let temp_dir = TempDir::new().unwrap();
316 let path = temp_dir.path();
317
318 // No resource files
319 assert_eq!(detect_resource_type(path), None);
320 }
321
322 #[test]
323 fn test_detect_resource_type_prefers_agent() {
324 let temp_dir = TempDir::new().unwrap();
325 let path = temp_dir.path();
326
327 // Create both files - agent should take precedence
328 fs::write(path.join("agent.toml"), "").unwrap();
329 fs::write(path.join("snippet.toml"), "").unwrap();
330
331 assert_eq!(detect_resource_type(path), Some(ResourceType::Agent));
332 }
333}