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 WorkspaceNotFound {
51 path: String,
52 },
53 ParallelExecutionFailed {
55 failed_scripts: Vec<String>,
56 },
57 TemplateNotFound {
59 name: String,
60 available: Vec<String>,
61 },
62 CargoScriptNotAvailable {
64 suggestion: String,
65 },
66 HookFailed {
68 hook_name: String,
69 script_name: String,
70 reason: String,
71 },
72 WatchError {
74 path: String,
75 message: String,
76 },
77 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 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
365fn 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 candidates.sort_by_key(|(_, d)| *d);
381 candidates
382 .into_iter()
383 .take(3)
384 .filter(|(_, d)| *d <= query.len().max(3)) .map(|(s, _)| s)
386 .collect()
387}
388
389fn 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
424pub 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
442pub 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