1use super::*;
7use crate::core::file_error::FileOperationError;
8
9const TEMPLATE_ERROR_KEYWORDS: &[&str] = &["template", "variable", "filter"];
11
12const NETWORK_ERROR_KEYWORDS: &[&str] = &["network", "connection", "timeout"];
14
15const GIT_ERROR_KEYWORDS: &[&str] = &["git command", "git operation", "git clone", "git fetch"];
17
18const PERMISSION_ERROR_KEYWORDS: &[&str] = &["permission", "denied", "access"];
20
21#[must_use]
36pub fn user_friendly_error(error: anyhow::Error) -> ErrorContext {
37 if let Some(ccmp_error) = error.downcast_ref::<AgpmError>() {
39 return create_error_context(ccmp_error);
40 }
41
42 let mut current_error: &dyn std::error::Error = error.as_ref();
44 loop {
45 if let Some(agpm_error) = current_error.downcast_ref::<AgpmError>() {
47 return create_error_context(agpm_error);
50 }
51
52 if let Some(template_error) =
54 current_error.downcast_ref::<crate::templating::TemplateError>()
55 {
56 let formatted = template_error.format_with_context();
58 return ErrorContext::new(AgpmError::Other {
59 message: formatted.clone(),
60 })
61 .with_suggestion("Check your template syntax and variable declarations")
62 .with_details(formatted);
63 }
64
65 match current_error.source() {
67 Some(source) => current_error = source,
68 None => break,
69 }
70 }
71
72 if let Some(file_error) = error.downcast_ref::<FileOperationError>() {
73 if file_error.source.kind() == std::io::ErrorKind::PermissionDenied {
75 return ErrorContext::new(AgpmError::PermissionDenied {
76 operation: file_error.operation.to_string(),
77 path: file_error.file_path.to_string_lossy().to_string(),
78 })
79 .with_suggestion("Check file permissions and try running with appropriate privileges")
80 .with_details(format!(
81 "Permission denied for '{}' on path: {}",
82 file_error.operation,
83 file_error.file_path.display()
84 ));
85 }
86
87 return ErrorContext::new(AgpmError::FileSystemError {
88 operation: file_error.operation.to_string(),
89 path: file_error.file_path.to_string_lossy().to_string(),
90 })
91 .with_suggestion("Check that the path exists and you have the necessary permissions")
92 .with_details(format!(
93 "Failed to {} at path: {}",
94 file_error.operation,
95 file_error.file_path.display()
96 ));
97 }
98
99 if let Some(io_error) = error.downcast_ref::<std::io::Error>() {
100 match io_error.kind() {
101 std::io::ErrorKind::PermissionDenied => {
102 return create_error_context(&AgpmError::PermissionDenied {
103 operation: "file access".to_string(),
104 path: "file path not specified in error context".to_string(),
105 });
106 }
107 std::io::ErrorKind::NotFound => {
108 return create_error_context(&AgpmError::FileSystemError {
109 operation: "file not found".to_string(),
110 path: "file path not specified in error context".to_string(),
111 });
112 }
113 std::io::ErrorKind::AlreadyExists => {
114 return create_error_context(&AgpmError::FileSystemError {
115 operation: "file creation".to_string(),
116 path: "file path not specified in error context".to_string(),
117 });
118 }
119 _ => {
120 return ErrorContext::new(AgpmError::FileSystemError {
121 operation: "file operation".to_string(),
122 path: "unknown path".to_string(),
123 })
124 .with_suggestion("Check file permissions and disk space")
125 .with_details(format!("IO error: {}", io_error));
126 }
127 }
128 }
129
130 let mut current_error: &dyn std::error::Error = error.as_ref();
132 loop {
133 let error_msg = current_error.to_string();
134
135 if error_msg.contains("No tags found") || error_msg.contains("No tag found") {
137 return ErrorContext::new(AgpmError::Other {
138 message: error_msg.clone(),
139 })
140 .with_suggestion("Check available tags with 'git tag -l' in the source repository, or adjust your version constraint")
141 .with_details("No tags match the requested version constraint");
142 }
143
144 match current_error.source() {
146 Some(source) => current_error = source,
147 None => break,
148 }
149 }
150
151 let error_msg = error.to_string();
153
154 if TEMPLATE_ERROR_KEYWORDS.iter().any(|&keyword| error_msg.contains(keyword)) {
156 return ErrorContext::new(AgpmError::Other {
157 message: format!("Template error: {}", error_msg),
158 })
159 .with_suggestion("Check your template syntax and variable names")
160 .with_details("Template rendering failed. Make sure all variables are defined and the syntax is correct.");
161 }
162
163 if NETWORK_ERROR_KEYWORDS.iter().any(|&keyword| error_msg.contains(keyword)) {
165 return ErrorContext::new(AgpmError::NetworkError {
166 operation: "network request".to_string(),
167 reason: error_msg.clone(),
168 })
169 .with_suggestion("Check your internet connection and try again")
170 .with_details("A network operation failed. Please verify your connection and retry.");
171 }
172
173 if GIT_ERROR_KEYWORDS.iter().any(|&keyword| error_msg.contains(keyword)) {
175 return ErrorContext::new(AgpmError::GitCommandError {
176 operation: "git operation".to_string(),
177 stderr: error_msg.clone(),
178 })
179 .with_suggestion("Ensure git is installed and configured correctly")
180 .with_details(
181 "A git operation failed. Check that git is in your PATH and properly configured.",
182 );
183 }
184
185 if PERMISSION_ERROR_KEYWORDS.iter().any(|&keyword| error_msg.contains(keyword)) {
188 return ErrorContext::new(AgpmError::Other {
189 message: error_msg.clone(),
190 })
191 .with_suggestion("Check file permissions and try running with appropriate privileges")
192 .with_details("Permission was denied for the requested operation.");
193 }
194
195 ErrorContext::new(AgpmError::Other {
197 message: error_msg,
198 })
199 .with_suggestion("Check the error message above for more details")
200 .with_details("An unexpected error occurred. Please report this issue if it persists.")
201}
202
203pub fn create_error_context(error: &AgpmError) -> ErrorContext {
210 match &error {
211 AgpmError::GitNotFound => ErrorContext::new(AgpmError::GitNotFound)
212 .with_suggestion("Install git from https://git-scm.com/ or your package manager")
213 .with_details("AGPM requires git to be installed and available in your PATH"),
214 AgpmError::ManifestNotFound => ErrorContext::new(AgpmError::ManifestNotFound)
215 .with_suggestion("Run 'agpm init' to create a new manifest, or navigate to a directory with an existing agpm.toml")
216 .with_details("AGPM searches for agpm.toml in the current directory and parent directories"),
217 AgpmError::GitCommandError {
218 operation,
219 stderr,
220 } => {
221 let suggestion = match operation.as_str() {
222 "fetch" => "Check your internet connection and try again",
223 "checkout" => "Verify the branch, tag, or commit reference exists",
224 "pull" => "Check your git configuration and remote settings",
225 "clone" => "Verify the repository URL and your network connection",
226 _ => "Ensure git is properly configured and try again",
227 };
228 ErrorContext::new(AgpmError::GitCommandError {
229 operation: operation.clone(),
230 stderr: stderr.clone(),
231 })
232 .with_suggestion(suggestion)
233 .with_details(format!("Git {} operation failed: {}", operation, stderr))
234 }
235 AgpmError::GitCloneFailed {
236 url,
237 reason,
238 } => ErrorContext::new(AgpmError::GitCloneFailed {
239 url: url.clone(),
240 reason: reason.clone(),
241 })
242 .with_suggestion(format!("Verify the repository URL '{}' is correct and accessible", url))
243 .with_details(format!("Failed to clone repository: {}", reason)),
244 AgpmError::ResourceNotFound {
245 name,
246 } => ErrorContext::new(AgpmError::ResourceNotFound {
247 name: name.clone(),
248 })
249 .with_suggestion("Check that the resource is installed and available")
250 .with_details(format!("Resource '{}' not found", name)),
251 AgpmError::ResourceFileNotFound {
252 path,
253 source_name,
254 } => ErrorContext::new(AgpmError::ResourceFileNotFound {
255 path: path.clone(),
256 source_name: source_name.clone(),
257 })
258 .with_suggestion(format!(
259 "Check that '{}' exists in source '{}' and the version/tag is correct",
260 path, source_name
261 ))
262 .with_details(format!("Resource file '{}' not found in source '{}'", path, source_name)),
263 AgpmError::ManifestParseError {
264 file,
265 reason,
266 } => ErrorContext::new(AgpmError::ManifestParseError {
267 file: file.clone(),
268 reason: reason.clone(),
269 })
270 .with_suggestion(format!("Check the syntax in '{}' - TOML format must be valid", file))
271 .with_details(format!("Failed to parse manifest file: {}", reason)),
272 AgpmError::FileSystemError {
273 operation,
274 path,
275 } => ErrorContext::new(AgpmError::FileSystemError {
276 operation: operation.clone(),
277 path: path.clone(),
278 })
279 .with_suggestion("Check that the path exists and you have the necessary permissions")
280 .with_details(format!("Failed to {} at path: {}", operation, path)),
281 AgpmError::PermissionDenied {
282 operation,
283 path,
284 } => ErrorContext::new(AgpmError::PermissionDenied {
285 operation: operation.clone(),
286 path: path.clone(),
287 })
288 .with_suggestion("Check file permissions and try running with appropriate privileges")
289 .with_details(format!("Permission denied for '{}' on path: {}", operation, path)),
290 _ => ErrorContext::new(AgpmError::Other {
292 message: error.to_string(),
293 })
294 .with_suggestion("Check the error message above for more details")
295 .with_details("An unexpected error occurred. Please report this issue if it persists."),
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use std::io;
303
304 #[test]
305 fn test_user_friendly_error_io_permission_denied() {
306 let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "Access denied");
307 let error = anyhow::Error::from(io_err);
308 let ctx = user_friendly_error(error);
309
310 assert!(matches!(ctx.error, AgpmError::PermissionDenied { .. }));
312 assert!(ctx.suggestion.is_some());
313 }
314
315 #[test]
316 fn test_user_friendly_error_io_not_found() {
317 let io_err = io::Error::new(io::ErrorKind::NotFound, "File not found");
318 let error = anyhow::Error::from(io_err);
319 let ctx = user_friendly_error(error);
320
321 assert!(matches!(ctx.error, AgpmError::FileSystemError { .. }));
322 assert!(ctx.suggestion.is_some());
323 }
324
325 #[test]
326 fn test_user_friendly_error_template_error() {
327 let error = anyhow::Error::msg("Failed to render template: variable 'foo' not found");
328 let ctx = user_friendly_error(error);
329
330 assert!(ctx.suggestion.is_some());
332 }
333
334 #[test]
335 fn test_user_friendly_error_network_error() {
336 let error = anyhow::Error::msg("Network connection failed");
337 let ctx = user_friendly_error(error);
338
339 assert!(matches!(ctx.error, AgpmError::NetworkError { .. }));
340 assert!(ctx.suggestion.is_some());
341 assert!(ctx.suggestion.unwrap().contains("internet connection"));
342 }
343
344 #[test]
345 fn test_user_friendly_error_git_error() {
346 let error = anyhow::Error::msg("git command failed: repository not found");
347 let ctx = user_friendly_error(error);
348
349 assert!(matches!(ctx.error, AgpmError::GitCommandError { .. }));
350 assert!(ctx.suggestion.is_some());
351 assert!(ctx.suggestion.unwrap().contains("git is installed"));
352 }
353
354 #[test]
355 fn test_user_friendly_error_fallback() {
356 let error = anyhow::Error::msg("Some completely unknown error type");
357 let ctx = user_friendly_error(error);
358
359 assert!(matches!(ctx.error, AgpmError::Other { .. }));
360 assert!(ctx.suggestion.is_some());
361 }
363}