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}