1use colored::*;
6use std::fmt;
7
8#[derive(Debug)]
10pub enum CargoScriptError {
11 ScriptFileNotFound {
13 path: String,
14 source: std::io::Error,
15 },
16 InvalidToml {
18 path: String,
19 message: String,
20 line: Option<usize>,
21 },
22 ScriptNotFound {
24 script_name: String,
25 available_scripts: Vec<String>,
26 },
27 ToolNotFound {
29 tool: String,
30 required_version: Option<String>,
31 suggestion: String,
32 },
33 ToolchainNotFound {
35 toolchain: String,
36 suggestion: String,
37 },
38 ExecutionError {
40 script: String,
41 command: String,
42 source: std::io::Error,
43 },
44 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 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
236fn 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 candidates.sort_by_key(|(_, d)| *d);
252 candidates
253 .into_iter()
254 .take(3)
255 .filter(|(_, d)| *d <= query.len().max(3)) .map(|(s, _)| s)
257 .collect()
258}
259
260fn 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
295pub 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
313pub 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