cargo_run/
error.rs

1//! Error handling module for cargo-script CLI tool.
2//!
3//! This module provides custom error types and utilities for better error messages.
4
5use colored::*;
6use std::fmt;
7
8/// Custom error type for cargo-script operations.
9#[derive(Debug)]
10pub enum CargoScriptError {
11    /// Script file not found or cannot be read
12    ScriptFileNotFound {
13        path: String,
14        source: std::io::Error,
15    },
16    /// Invalid TOML syntax in Scripts.toml
17    InvalidToml {
18        path: String,
19        message: String,
20        line: Option<usize>,
21    },
22    /// Script not found in Scripts.toml
23    ScriptNotFound {
24        script_name: String,
25        available_scripts: Vec<String>,
26    },
27    /// Required tool is missing or wrong version
28    ToolNotFound {
29        tool: String,
30        required_version: Option<String>,
31        suggestion: String,
32    },
33    /// Toolchain not installed
34    ToolchainNotFound {
35        toolchain: String,
36        suggestion: String,
37    },
38    /// Script execution error
39    ExecutionError {
40        script: String,
41        command: String,
42        source: std::io::Error,
43    },
44    /// Windows self-replacement error (trying to replace cargo-script while it's running)
45    WindowsSelfReplacementError {
46        script: String,
47        command: String,
48    },
49}
50
51impl fmt::Display for CargoScriptError {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        match self {
54            CargoScriptError::ScriptFileNotFound { path, source } => {
55                write!(
56                    f,
57                    "{}\n\n{}\n  {}\n  {}\n\n{}\n  {}\n  {}",
58                    "❌ Script file not found".red().bold(),
59                    "Error:".yellow().bold(),
60                    format!("Path: {}", path).white(),
61                    format!("Reason: {}", source).white(),
62                    "Quick fix:".yellow().bold(),
63                    format!("Run '{}' to create Scripts.toml in the current directory", "cargo script init".green()).white(),
64                    format!("Or use '{}' to specify a different file path", "--scripts-path <path>".green()).white()
65                )
66            }
67            CargoScriptError::InvalidToml { path, message, line } => {
68                let line_info = if let Some(l) = line {
69                    format!("\n  Line {}: {}", l, "See error details above".yellow())
70                } else {
71                    String::new()
72                };
73                write!(
74                    f,
75                    "{}\n\n{}\n  {}\n  {}{}\n\n{}\n  {}\n  {}\n  {}",
76                    "❌ Invalid TOML syntax".red().bold(),
77                    "Error:".yellow().bold(),
78                    format!("File: {}", path).white(),
79                    format!("Message: {}", message).white(),
80                    line_info,
81                    "Quick fix:".yellow().bold(),
82                    "Check your Scripts.toml syntax. Common issues:".white(),
83                    "  - Missing quotes around strings\n  - Trailing commas in arrays\n  - Invalid table syntax".white(),
84                    format!("Validate your file with: {}", "cargo script validate".green()).white()
85                )
86            }
87            CargoScriptError::ScriptNotFound {
88                script_name,
89                available_scripts,
90            } => {
91                let suggestions = find_similar_scripts(script_name, available_scripts);
92                let suggestion_text = if !suggestions.is_empty() {
93                    format!(
94                        "\n\n{}\n  {}",
95                        "Did you mean:".yellow().bold(),
96                        suggestions
97                            .iter()
98                            .map(|s| format!("  • {}", s.green()))
99                            .collect::<Vec<_>>()
100                            .join("\n")
101                    )
102                } else if !available_scripts.is_empty() {
103                    format!(
104                        "\n\n{}\n  {}",
105                        "Available scripts:".yellow().bold(),
106                        available_scripts
107                            .iter()
108                            .take(10)
109                            .map(|s| format!("  • {}", s.cyan()))
110                            .collect::<Vec<_>>()
111                            .join("\n")
112                    )
113                } else {
114                    String::new()
115                };
116
117                write!(
118                    f,
119                    "{}\n\n{}\n  {}{}\n\n{}\n  {}\n  {}",
120                    "❌ Script not found".red().bold(),
121                    "Error:".yellow().bold(),
122                    format!("Script '{}' not found in Scripts.toml", script_name.bold()).white(),
123                    suggestion_text,
124                    "Quick fix:".yellow().bold(),
125                    format!("Run '{}' to see all available scripts", "cargo script show".green()).white(),
126                    format!("Or use '{}' to initialize Scripts.toml if it doesn't exist", "cargo script init".green()).white()
127                )
128            }
129            CargoScriptError::ToolNotFound {
130                tool,
131                required_version,
132                suggestion,
133            } => {
134                let version_info = if let Some(v) = required_version {
135                    format!(" (required: {})", v)
136                } else {
137                    String::new()
138                };
139                write!(
140                    f,
141                    "{}\n\n{}\n  {}{}\n\n{}\n  {}",
142                    "❌ Required tool not found".red().bold(),
143                    "Error:".yellow().bold(),
144                    format!("Tool '{}'{} is not installed or not in PATH", tool.bold(), version_info).white(),
145                    suggestion,
146                    "Suggestion:".yellow().bold(),
147                    format!("Install '{}' and ensure it's available in your PATH", tool).white()
148                )
149            }
150            CargoScriptError::ToolchainNotFound {
151                toolchain,
152                suggestion,
153            } => {
154                write!(
155                    f,
156                    "{}\n\n{}\n  {}\n\n{}\n{}",
157                    "❌ Toolchain not installed".red().bold(),
158                    "Error:".yellow().bold(),
159                    format!("Toolchain '{}' is not installed", toolchain.bold()).white(),
160                    "Suggestion:".yellow().bold(),
161                    suggestion
162                )
163            }
164            CargoScriptError::ExecutionError {
165                script,
166                command,
167                source,
168            } => {
169                // Check if this is a Windows self-replacement error
170                let is_windows_self_replace = cfg!(target_os = "windows")
171                    && (command.contains("cargo install --path .") || command.contains("cargo install --path"))
172                    && (source.to_string().contains("Access is denied") 
173                        || source.to_string().contains("os error 5")
174                        || source.to_string().contains("failed to move"));
175
176                if is_windows_self_replace {
177                    write!(
178                        f,
179                        "{}\n\n{}\n  {}\n  {}\n\n{}\n  {}\n  {}\n  {}\n\n{}\n  {}",
180                        "❌ Cannot replace cargo-script while it's running (Windows limitation)".red().bold(),
181                        "Error:".yellow().bold(),
182                        format!("Script: {}", script.bold()).white(),
183                        format!("Command: {}", command).white(),
184                        "Why:".yellow().bold(),
185                        "Windows locks executable files while they're running for security and stability.".white(),
186                        "When cargo-script runs 'cargo install --path .', it tries to replace itself,".white(),
187                        "but Windows prevents this because cargo-script.exe is currently in use.".white(),
188                        "Solution:".yellow().bold(),
189                        format!("Run '{}' directly in your terminal (not via cargo script)", command.green()).white()
190                    )
191                } else {
192                    write!(
193                        f,
194                        "{}\n\n{}\n  {}\n  {}\n  {}\n\n{}\n  {}",
195                        "❌ Script execution failed".red().bold(),
196                        "Error:".yellow().bold(),
197                        format!("Script: {}", script.bold()).white(),
198                        format!("Command: {}", command).white(),
199                        format!("Reason: {}", source).white(),
200                        "Suggestion:".yellow().bold(),
201                        "Check the command syntax and ensure all required tools are installed".white()
202                    )
203                }
204            }
205            CargoScriptError::WindowsSelfReplacementError { script, command } => {
206                write!(
207                    f,
208                    "{}\n\n{}\n  {}\n  {}\n\n{}\n  {}\n  {}\n  {}\n\n{}\n  {}",
209                    "❌ Cannot replace cargo-script while it's running (Windows limitation)".red().bold(),
210                    "Error:".yellow().bold(),
211                    format!("Script: {}", script.bold()).white(),
212                    format!("Command: {}", command).white(),
213                    "Why:".yellow().bold(),
214                    "Windows locks executable files while they're running for security and stability.".white(),
215                    "When cargo-script runs 'cargo install --path .', it tries to replace itself,".white(),
216                    "but Windows prevents this because cargo-script.exe is currently in use.".white(),
217                    "Solution:".yellow().bold(),
218                    format!("Run '{}' directly in your terminal (not via cargo script)", command.green()).white()
219                )
220            }
221        }
222    }
223}
224
225impl std::error::Error for CargoScriptError {
226    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
227        match self {
228            CargoScriptError::ScriptFileNotFound { source, .. } => Some(source),
229            CargoScriptError::ExecutionError { source, .. } => Some(source),
230            CargoScriptError::WindowsSelfReplacementError { .. } => None,
231            _ => None,
232        }
233    }
234}
235
236/// Find similar script names using Levenshtein distance.
237fn find_similar_scripts(query: &str, available: &[String]) -> Vec<String> {
238    if available.is_empty() {
239        return Vec::new();
240    }
241
242    let mut candidates: Vec<(String, usize)> = available
243        .iter()
244        .map(|s| {
245            let distance = levenshtein_distance(query, s);
246            (s.clone(), distance)
247        })
248        .collect();
249
250    // Sort by distance and take the top 3 matches
251    candidates.sort_by_key(|(_, d)| *d);
252    candidates
253        .into_iter()
254        .take(3)
255        .filter(|(_, d)| *d <= query.len().max(3)) // Only suggest if reasonably close
256        .map(|(s, _)| s)
257        .collect()
258}
259
260/// Calculate Levenshtein distance between two strings.
261fn levenshtein_distance(s1: &str, s2: &str) -> usize {
262    let s1_chars: Vec<char> = s1.chars().collect();
263    let s2_chars: Vec<char> = s2.chars().collect();
264    let s1_len = s1_chars.len();
265    let s2_len = s2_chars.len();
266
267    if s1_len == 0 {
268        return s2_len;
269    }
270    if s2_len == 0 {
271        return s1_len;
272    }
273
274    let mut matrix = vec![vec![0; s2_len + 1]; s1_len + 1];
275
276    for i in 0..=s1_len {
277        matrix[i][0] = i;
278    }
279    for j in 0..=s2_len {
280        matrix[0][j] = j;
281    }
282
283    for i in 1..=s1_len {
284        for j in 1..=s2_len {
285            let cost = if s1_chars[i - 1] == s2_chars[j - 1] { 0 } else { 1 };
286            matrix[i][j] = (matrix[i - 1][j] + 1)
287                .min(matrix[i][j - 1] + 1)
288                .min(matrix[i - 1][j - 1] + cost);
289        }
290    }
291
292    matrix[s1_len][s2_len]
293}
294
295/// Helper function to create a tool not found error with installation suggestions.
296pub fn create_tool_not_found_error(tool: &str, required_version: Option<&str>) -> CargoScriptError {
297    let suggestion = match tool {
298        "rustup" => "Install rustup: https://rustup.rs/".to_string(),
299        "cargo" => "Install Rust: https://www.rust-lang.org/tools/install".to_string(),
300        "python" => "Install Python: https://www.python.org/downloads/".to_string(),
301        "docker" => "Install Docker: https://docs.docker.com/get-docker/".to_string(),
302        "kubectl" => "Install kubectl: https://kubernetes.io/docs/tasks/tools/".to_string(),
303        _ => format!("Install {} from your package manager or official website", tool),
304    };
305
306    CargoScriptError::ToolNotFound {
307        tool: tool.to_string(),
308        required_version: required_version.map(|s| s.to_string()),
309        suggestion: format!("  {}", suggestion.cyan()),
310    }
311}
312
313/// Helper function to create a toolchain not found error.
314pub fn create_toolchain_not_found_error(toolchain: &str) -> CargoScriptError {
315    let suggestion = if toolchain.starts_with("python:") {
316        format!("Install Python {} using your system package manager", toolchain.replace("python:", ""))
317    } else {
318        format!("Install toolchain: rustup toolchain install {}", toolchain)
319    };
320
321    CargoScriptError::ToolchainNotFound {
322        toolchain: toolchain.to_string(),
323        suggestion: format!("  {}", suggestion.cyan()),
324    }
325}
326