Skip to main content

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    /// Workspace `Cargo.toml` could not be found at or above the given path
50    WorkspaceNotFound {
51        path: String,
52    },
53    /// One or more scripts failed during a parallel execution
54    ParallelExecutionFailed {
55        failed_scripts: Vec<String>,
56    },
57    /// Requested template does not exist in the registry
58    TemplateNotFound {
59        name: String,
60        available: Vec<String>,
61    },
62    /// `cargo script` (single-file packages) is not available on the current toolchain
63    CargoScriptNotAvailable {
64        suggestion: String,
65    },
66    /// A pre/post/on_success/on_failure hook failed
67    HookFailed {
68        hook_name: String,
69        script_name: String,
70        reason: String,
71    },
72    /// Watch mode error (file system event subscription failed)
73    WatchError {
74        path: String,
75        message: String,
76    },
77    /// A required script argument/parameter was not provided
78    MissingScriptArgument {
79        script_name: String,
80        argument: String,
81    },
82}
83
84impl fmt::Display for CargoScriptError {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        match self {
87            CargoScriptError::ScriptFileNotFound { path, source } => {
88                write!(
89                    f,
90                    "{}\n\n{}\n  {}\n  {}\n\n{}\n  {}\n  {}",
91                    "❌ Script file not found".red().bold(),
92                    "Error:".yellow().bold(),
93                    format!("Path: {}", path).white(),
94                    format!("Reason: {}", source).white(),
95                    "Quick fix:".yellow().bold(),
96                    format!("Run '{}' to create Scripts.toml in the current directory", "cargo script init".green()).white(),
97                    format!("Or use '{}' to specify a different file path", "--scripts-path <path>".green()).white()
98                )
99            }
100            CargoScriptError::InvalidToml { path, message, line } => {
101                let line_info = if let Some(l) = line {
102                    format!("\n  Line {}: {}", l, "See error details above".yellow())
103                } else {
104                    String::new()
105                };
106                write!(
107                    f,
108                    "{}\n\n{}\n  {}\n  {}{}\n\n{}\n  {}\n  {}\n  {}",
109                    "❌ Invalid TOML syntax".red().bold(),
110                    "Error:".yellow().bold(),
111                    format!("File: {}", path).white(),
112                    format!("Message: {}", message).white(),
113                    line_info,
114                    "Quick fix:".yellow().bold(),
115                    "Check your Scripts.toml syntax. Common issues:".white(),
116                    "  - Missing quotes around strings\n  - Trailing commas in arrays\n  - Invalid table syntax".white(),
117                    format!("Validate your file with: {}", "cargo script validate".green()).white()
118                )
119            }
120            CargoScriptError::ScriptNotFound {
121                script_name,
122                available_scripts,
123            } => {
124                let suggestions = find_similar_scripts(script_name, available_scripts);
125                let suggestion_text = if !suggestions.is_empty() {
126                    format!(
127                        "\n\n{}\n  {}",
128                        "Did you mean:".yellow().bold(),
129                        suggestions
130                            .iter()
131                            .map(|s| format!("  • {}", s.green()))
132                            .collect::<Vec<_>>()
133                            .join("\n")
134                    )
135                } else if !available_scripts.is_empty() {
136                    format!(
137                        "\n\n{}\n  {}",
138                        "Available scripts:".yellow().bold(),
139                        available_scripts
140                            .iter()
141                            .take(10)
142                            .map(|s| format!("  • {}", s.cyan()))
143                            .collect::<Vec<_>>()
144                            .join("\n")
145                    )
146                } else {
147                    String::new()
148                };
149
150                write!(
151                    f,
152                    "{}\n\n{}\n  {}{}\n\n{}\n  {}\n  {}",
153                    "❌ Script not found".red().bold(),
154                    "Error:".yellow().bold(),
155                    format!("Script '{}' not found in Scripts.toml", script_name.bold()).white(),
156                    suggestion_text,
157                    "Quick fix:".yellow().bold(),
158                    format!("Run '{}' to see all available scripts", "cargo script show".green()).white(),
159                    format!("Or use '{}' to initialize Scripts.toml if it doesn't exist", "cargo script init".green()).white()
160                )
161            }
162            CargoScriptError::ToolNotFound {
163                tool,
164                required_version,
165                suggestion,
166            } => {
167                let version_info = if let Some(v) = required_version {
168                    format!(" (required: {})", v)
169                } else {
170                    String::new()
171                };
172                write!(
173                    f,
174                    "{}\n\n{}\n  {}{}\n\n{}\n  {}",
175                    "❌ Required tool not found".red().bold(),
176                    "Error:".yellow().bold(),
177                    format!("Tool '{}'{} is not installed or not in PATH", tool.bold(), version_info).white(),
178                    suggestion,
179                    "Suggestion:".yellow().bold(),
180                    format!("Install '{}' and ensure it's available in your PATH", tool).white()
181                )
182            }
183            CargoScriptError::ToolchainNotFound {
184                toolchain,
185                suggestion,
186            } => {
187                write!(
188                    f,
189                    "{}\n\n{}\n  {}\n\n{}\n{}",
190                    "❌ Toolchain not installed".red().bold(),
191                    "Error:".yellow().bold(),
192                    format!("Toolchain '{}' is not installed", toolchain.bold()).white(),
193                    "Suggestion:".yellow().bold(),
194                    suggestion
195                )
196            }
197            CargoScriptError::ExecutionError {
198                script,
199                command,
200                source,
201            } => {
202                // Check if this is a Windows self-replacement error
203                let is_windows_self_replace = cfg!(target_os = "windows")
204                    && (command.contains("cargo install --path .") || command.contains("cargo install --path"))
205                    && (source.to_string().contains("Access is denied") 
206                        || source.to_string().contains("os error 5")
207                        || source.to_string().contains("failed to move"));
208
209                if is_windows_self_replace {
210                    write!(
211                        f,
212                        "{}\n\n{}\n  {}\n  {}\n\n{}\n  {}\n  {}\n  {}\n\n{}\n  {}",
213                        "❌ Cannot replace cargo-script while it's running (Windows limitation)".red().bold(),
214                        "Error:".yellow().bold(),
215                        format!("Script: {}", script.bold()).white(),
216                        format!("Command: {}", command).white(),
217                        "Why:".yellow().bold(),
218                        "Windows locks executable files while they're running for security and stability.".white(),
219                        "When cargo-script runs 'cargo install --path .', it tries to replace itself,".white(),
220                        "but Windows prevents this because cargo-script.exe is currently in use.".white(),
221                        "Solution:".yellow().bold(),
222                        format!("Run '{}' directly in your terminal (not via cargo script)", command.green()).white()
223                    )
224                } else {
225                    write!(
226                        f,
227                        "{}\n\n{}\n  {}\n  {}\n  {}\n\n{}\n  {}",
228                        "❌ Script execution failed".red().bold(),
229                        "Error:".yellow().bold(),
230                        format!("Script: {}", script.bold()).white(),
231                        format!("Command: {}", command).white(),
232                        format!("Reason: {}", source).white(),
233                        "Suggestion:".yellow().bold(),
234                        "Check the command syntax and ensure all required tools are installed".white()
235                    )
236                }
237            }
238            CargoScriptError::WindowsSelfReplacementError { script, command } => {
239                write!(
240                    f,
241                    "{}\n\n{}\n  {}\n  {}\n\n{}\n  {}\n  {}\n  {}\n\n{}\n  {}",
242                    "❌ Cannot replace cargo-script while it's running (Windows limitation)".red().bold(),
243                    "Error:".yellow().bold(),
244                    format!("Script: {}", script.bold()).white(),
245                    format!("Command: {}", command).white(),
246                    "Why:".yellow().bold(),
247                    "Windows locks executable files while they're running for security and stability.".white(),
248                    "When cargo-script runs 'cargo install --path .', it tries to replace itself,".white(),
249                    "but Windows prevents this because cargo-script.exe is currently in use.".white(),
250                    "Solution:".yellow().bold(),
251                    format!("Run '{}' directly in your terminal (not via cargo script)", command.green()).white()
252                )
253            }
254            CargoScriptError::WorkspaceNotFound { path } => {
255                write!(
256                    f,
257                    "{}\n\n{}\n  {}\n\n{}\n  {}\n  {}",
258                    "❌ Workspace not found".red().bold(),
259                    "Error:".yellow().bold(),
260                    format!("No Cargo.toml with a [workspace] section was found at or above '{}'", path).white(),
261                    "Quick fix:".yellow().bold(),
262                    "Run cargo-run from inside a Cargo workspace, or".white(),
263                    "explicitly declare members in [workspace] of your Scripts.toml.".white(),
264                )
265            }
266            CargoScriptError::ParallelExecutionFailed { failed_scripts } => {
267                let list = failed_scripts
268                    .iter()
269                    .map(|s| format!("    - {}", s.red()))
270                    .collect::<Vec<_>>()
271                    .join("\n");
272                write!(
273                    f,
274                    "{}\n\n{}\n  {} script(s) failed in parallel execution:\n{}",
275                    "❌ Parallel execution failed".red().bold(),
276                    "Error:".yellow().bold(),
277                    failed_scripts.len(),
278                    list,
279                )
280            }
281            CargoScriptError::TemplateNotFound { name, available } => {
282                let list = if available.is_empty() {
283                    "  (no templates registered)".to_string()
284                } else {
285                    available
286                        .iter()
287                        .map(|t| format!("  • {}", t.green()))
288                        .collect::<Vec<_>>()
289                        .join("\n")
290                };
291                write!(
292                    f,
293                    "{}\n\n{}\n  Template '{}' is not registered\n\n{}\n{}\n\n{}\n  {}",
294                    "❌ Template not found".red().bold(),
295                    "Error:".yellow().bold(),
296                    name.bold(),
297                    "Available templates:".yellow().bold(),
298                    list,
299                    "Quick fix:".yellow().bold(),
300                    format!("Run '{}' to list templates", "cargo script init --list-templates".green()).white(),
301                )
302            }
303            CargoScriptError::CargoScriptNotAvailable { suggestion } => {
304                write!(
305                    f,
306                    "{}\n\n{}\n  cargo-script (single-file Rust packages) is not available\n\n{}\n{}",
307                    "❌ cargo script not available".red().bold(),
308                    "Error:".yellow().bold(),
309                    "Suggestion:".yellow().bold(),
310                    suggestion,
311                )
312            }
313            CargoScriptError::HookFailed { hook_name, script_name, reason } => {
314                write!(
315                    f,
316                    "{}\n\n{}\n  Hook '{}' for script '{}' failed\n  Reason: {}\n\n{}\n  {}",
317                    "❌ Hook execution failed".red().bold(),
318                    "Error:".yellow().bold(),
319                    hook_name.bold(),
320                    script_name.bold(),
321                    reason,
322                    "Suggestion:".yellow().bold(),
323                    "Check that the hook script exists in Scripts.toml and exits successfully".white(),
324                )
325            }
326            CargoScriptError::WatchError { path, message } => {
327                write!(
328                    f,
329                    "{}\n\n{}\n  Failed to watch '{}'\n  {}\n\n{}\n  {}",
330                    "❌ Watch mode error".red().bold(),
331                    "Error:".yellow().bold(),
332                    path,
333                    message,
334                    "Suggestion:".yellow().bold(),
335                    "Ensure the path exists and the process has read access".white(),
336                )
337            }
338            CargoScriptError::MissingScriptArgument { script_name, argument } => {
339                write!(
340                    f,
341                    "{}\n\n{}\n  Script '{}' requires argument '{}'\n\n{}\n  {}",
342                    "❌ Missing script argument".red().bold(),
343                    "Error:".yellow().bold(),
344                    script_name.bold(),
345                    argument.bold(),
346                    "Quick fix:".yellow().bold(),
347                    format!("Pass it as: cargo script {} {}=<value>", script_name, argument).green().to_string(),
348                )
349            }
350        }
351    }
352}
353
354impl std::error::Error for CargoScriptError {
355    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
356        match self {
357            CargoScriptError::ScriptFileNotFound { source, .. } => Some(source),
358            CargoScriptError::ExecutionError { source, .. } => Some(source),
359            CargoScriptError::WindowsSelfReplacementError { .. } => None,
360            _ => None,
361        }
362    }
363}
364
365/// Find similar script names using Levenshtein distance.
366fn find_similar_scripts(query: &str, available: &[String]) -> Vec<String> {
367    if available.is_empty() {
368        return Vec::new();
369    }
370
371    let mut candidates: Vec<(String, usize)> = available
372        .iter()
373        .map(|s| {
374            let distance = levenshtein_distance(query, s);
375            (s.clone(), distance)
376        })
377        .collect();
378
379    // Sort by distance and take the top 3 matches
380    candidates.sort_by_key(|(_, d)| *d);
381    candidates
382        .into_iter()
383        .take(3)
384        .filter(|(_, d)| *d <= query.len().max(3)) // Only suggest if reasonably close
385        .map(|(s, _)| s)
386        .collect()
387}
388
389/// Calculate Levenshtein distance between two strings.
390fn levenshtein_distance(s1: &str, s2: &str) -> usize {
391    let s1_chars: Vec<char> = s1.chars().collect();
392    let s2_chars: Vec<char> = s2.chars().collect();
393    let s1_len = s1_chars.len();
394    let s2_len = s2_chars.len();
395
396    if s1_len == 0 {
397        return s2_len;
398    }
399    if s2_len == 0 {
400        return s1_len;
401    }
402
403    let mut matrix = vec![vec![0; s2_len + 1]; s1_len + 1];
404
405    for i in 0..=s1_len {
406        matrix[i][0] = i;
407    }
408    for j in 0..=s2_len {
409        matrix[0][j] = j;
410    }
411
412    for i in 1..=s1_len {
413        for j in 1..=s2_len {
414            let cost = if s1_chars[i - 1] == s2_chars[j - 1] { 0 } else { 1 };
415            matrix[i][j] = (matrix[i - 1][j] + 1)
416                .min(matrix[i][j - 1] + 1)
417                .min(matrix[i - 1][j - 1] + cost);
418        }
419    }
420
421    matrix[s1_len][s2_len]
422}
423
424/// Helper function to create a tool not found error with installation suggestions.
425pub fn create_tool_not_found_error(tool: &str, required_version: Option<&str>) -> CargoScriptError {
426    let suggestion = match tool {
427        "rustup" => "Install rustup: https://rustup.rs/".to_string(),
428        "cargo" => "Install Rust: https://www.rust-lang.org/tools/install".to_string(),
429        "python" => "Install Python: https://www.python.org/downloads/".to_string(),
430        "docker" => "Install Docker: https://docs.docker.com/get-docker/".to_string(),
431        "kubectl" => "Install kubectl: https://kubernetes.io/docs/tasks/tools/".to_string(),
432        _ => format!("Install {} from your package manager or official website", tool),
433    };
434
435    CargoScriptError::ToolNotFound {
436        tool: tool.to_string(),
437        required_version: required_version.map(|s| s.to_string()),
438        suggestion: format!("  {}", suggestion.cyan()),
439    }
440}
441
442/// Helper function to create a toolchain not found error.
443pub fn create_toolchain_not_found_error(toolchain: &str) -> CargoScriptError {
444    let suggestion = if toolchain.starts_with("python:") {
445        format!("Install Python {} using your system package manager", toolchain.replace("python:", ""))
446    } else {
447        format!("Install toolchain: rustup toolchain install {}", toolchain)
448    };
449
450    CargoScriptError::ToolchainNotFound {
451        toolchain: toolchain.to_string(),
452        suggestion: format!("  {}", suggestion.cyan()),
453    }
454}
455