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());
106//! }
107//! ResourceType::Snippet => {
108//! println!("Found snippet resource");
109//! println!("Install dir: {}", resource_type.default_directory());
110//! }
111//! ResourceType::Command => {
112//! println!("Found command resource");
113//! println!("Install dir: {}", resource_type.default_directory());
114//! }
115//! ResourceType::McpServer => {
116//! println!("Found MCP server configuration");
117//! println!("Install dir: {}", resource_type.default_directory());
118//! }
119//! ResourceType::Script => {
120//! println!("Found script resource");
121//! println!("Install dir: {}", resource_type.default_directory());
122//! }
123//! ResourceType::Hook => {
124//! println!("Found hook configuration");
125//! println!("Install dir: {}", resource_type.default_directory());
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}