cargo_e/
e_command_builder.rs

1use regex::Regex;
2use std::collections::{HashMap, HashSet};
3use std::env;
4use std::io::Read;
5use std::path::{Path, PathBuf};
6use std::process::Command;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::mpsc::{channel, Sender};
9use std::time::SystemTime;
10use which::which;
11
12use crate::e_cargocommand_ext::CargoProcessResult;
13use crate::e_cargocommand_ext::{CargoCommandExt, CargoDiagnostic, CargoProcessHandle};
14use crate::e_eventdispatcher::{
15    CallbackResponse, CallbackType, CargoDiagnosticLevel, EventDispatcher,
16};
17use crate::e_runner::GLOBAL_CHILDREN;
18use crate::e_target::{CargoTarget, TargetKind, TargetOrigin};
19use std::sync::{Arc, Mutex};
20
21#[derive(Debug, Clone, PartialEq, Copy)]
22pub enum TerminalError {
23    NotConnected,
24    NoTerminal,
25    NoError,
26}
27
28impl Default for TerminalError {
29    fn default() -> Self {
30        TerminalError::NoError
31    }
32}
33
34/// A builder that constructs a Cargo command for a given target.
35#[derive(Clone)]
36pub struct CargoCommandBuilder {
37    pub target_name: String,
38    pub manifest_path: PathBuf,
39    pub args: Vec<String>,
40    pub subcommand: String,
41    pub pid: Option<u32>,
42    pub alternate_cmd: Option<String>,
43    pub execution_dir: Option<PathBuf>,
44    pub suppressed_flags: HashSet<String>,
45    pub stdout_dispatcher: Option<Arc<EventDispatcher>>,
46    pub stderr_dispatcher: Option<Arc<EventDispatcher>>,
47    pub progress_dispatcher: Option<Arc<EventDispatcher>>,
48    pub stage_dispatcher: Option<Arc<EventDispatcher>>,
49    pub terminal_error_flag: Arc<Mutex<bool>>,
50    pub sender: Option<Arc<Mutex<Sender<TerminalError>>>>,
51    pub diagnostics: Arc<Mutex<Vec<CargoDiagnostic>>>,
52    pub is_filter: bool,
53}
54impl Default for CargoCommandBuilder {
55    fn default() -> Self {
56        Self::new(
57            &String::new(),
58            &PathBuf::from("Cargo.toml"),
59            "run".into(),
60            false,
61        )
62    }
63}
64impl CargoCommandBuilder {
65    /// Creates a new, empty builder.
66    pub fn new(target_name: &str, manifest: &PathBuf, subcommand: &str, is_filter: bool) -> Self {
67        let (sender, _receiver) = channel::<TerminalError>();
68        let sender = Arc::new(Mutex::new(sender));
69        let mut builder = CargoCommandBuilder {
70            target_name: target_name.to_owned(),
71            manifest_path: manifest.clone(),
72            args: Vec::new(),
73            subcommand: subcommand.to_string(),
74            pid: None,
75            alternate_cmd: None,
76            execution_dir: None,
77            suppressed_flags: HashSet::new(),
78            stdout_dispatcher: None,
79            stderr_dispatcher: None,
80            progress_dispatcher: None,
81            stage_dispatcher: None,
82            terminal_error_flag: Arc::new(Mutex::new(false)),
83            sender: Some(sender),
84            diagnostics: Arc::new(Mutex::new(Vec::<CargoDiagnostic>::new())),
85            is_filter,
86        };
87        builder.set_default_dispatchers();
88
89        builder
90    }
91
92    // Switch to passthrough mode when the terminal error is detected
93    fn switch_to_passthrough_mode<F>(self: Arc<Self>, on_spawn: F) -> anyhow::Result<u32>
94    where
95        F: FnOnce(u32, CargoProcessHandle),
96    {
97        let mut command = self.build_command();
98
99        // Now, spawn the cargo process in passthrough mode
100        let cargo_process_handle = command.spawn_cargo_passthrough(Arc::clone(&self));
101        let pid = cargo_process_handle.pid;
102        // Notify observer
103        on_spawn(pid, cargo_process_handle);
104
105        Ok(pid)
106    }
107
108    // Set up the default dispatchers, which includes error detection
109    fn set_default_dispatchers(&mut self) {
110        if !self.is_filter {
111            // If this is a filter, we don't need to set up dispatchers
112            return;
113        }
114        let sender = self.sender.clone().unwrap();
115
116        let mut stdout_dispatcher = EventDispatcher::new();
117        stdout_dispatcher.add_callback(
118            r"listening on",
119            Box::new(|line, _captures, _state, stats| {
120                println!("(STDOUT) Dispatcher caught: {}", line);
121                // Use a regex to capture a URL from the line.
122                if let Ok(url_regex) = Regex::new(r"(http://[^\s]+)") {
123                    if let Some(url_caps) = url_regex.captures(line) {
124                        if let Some(url_match) = url_caps.get(1) {
125                            let url = url_match.as_str();
126                            // Call open::that on the captured URL.
127                            if let Err(e) = open::that_detached(url) {
128                                eprintln!("Failed to open URL: {}. Error: {}", url, e);
129                            } else {
130                                println!("Opened URL: {}", url);
131                            }
132                        }
133                    }
134                } else {
135                    eprintln!("Failed to create URL regex");
136                }
137                let mut stats = stats.lock().unwrap();
138                if stats.build_finished_time.is_none() {
139                    let now = SystemTime::now();
140                    stats.build_finished_time = Some(now);
141                }
142                None
143            }),
144        );
145
146        stdout_dispatcher.add_callback(
147            r"BuildFinished",
148            Box::new(|line, _captures, _state, stats| {
149                println!("******* {}", line);
150                let mut stats = stats.lock().unwrap();
151                if stats.build_finished_time.is_none() {
152                    let now = SystemTime::now();
153                    stats.build_finished_time = Some(now);
154                }
155                None
156            }),
157        );
158        stdout_dispatcher.add_callback(
159            r"server listening at:",
160            Box::new(|line, _captures, state, stats| {
161                // If we're not already in multiline mode, this is the initial match.
162                if !state.load(Ordering::Relaxed) {
163                    println!("Matched 'server listening at:' in: {}", line);
164                    state.store(true, Ordering::Relaxed);
165                    Some(CallbackResponse {
166                        callback_type: CallbackType::Note, // Choose as appropriate
167                        message: Some(format!("Started multiline mode after: {}", line)),
168                        file: None,
169                        line: None,
170                        column: None,
171                        suggestion: None,
172                        terminal_status: None,
173                    })
174                } else {
175                    // We are in multiline mode; process subsequent lines.
176                    println!("Multiline callback received: {}", line);
177                    // Use a regex to capture a URL from the line.
178                    let url_regex = match Regex::new(r"(http://[^\s]+)") {
179                        Ok(regex) => regex,
180                        Err(e) => {
181                            eprintln!("Failed to create URL regex: {}", e);
182                            return None;
183                        }
184                    };
185                    if let Some(url_caps) = url_regex.captures(line) {
186                        let url = url_caps.get(1).unwrap().as_str();
187                        // Call open::that on the captured URL.
188                        match open::that_detached(url) {
189                            Ok(_) => println!("Opened URL: {}", url),
190                            Err(e) => eprintln!("Failed to open URL: {}. Error: {}", url, e),
191                        }
192                        let mut stats = stats.lock().unwrap();
193                        if stats.build_finished_time.is_none() {
194                            let now = SystemTime::now();
195                            stats.build_finished_time = Some(now);
196                        }
197                        // End multiline mode.
198                        state.store(false, Ordering::Relaxed);
199                        Some(CallbackResponse {
200                            callback_type: CallbackType::Note, // Choose as appropriate
201                            message: Some(format!("Captured and opened URL: {}", url)),
202                            file: None,
203                            line: None,
204                            column: None,
205                            suggestion: None,
206                            terminal_status: None,
207                        })
208                    } else {
209                        None
210                    }
211                }
212            }),
213        );
214
215        let mut stderr_dispatcher = EventDispatcher::new();
216
217        let suggestion_mode = Arc::new(AtomicBool::new(false));
218        let suggestion_regex = Regex::new(r"^\s*(\d+)\s*\|\s*(.*)$").unwrap();
219        let warning_location: Arc<Mutex<Option<CallbackResponse>>> = Arc::new(Mutex::new(None));
220        let pending_diag: Arc<Mutex<Option<CargoDiagnostic>>> = Arc::new(Mutex::new(None));
221        let diagnostic_counts: Arc<Mutex<HashMap<CargoDiagnosticLevel, usize>>> =
222            Arc::new(Mutex::new(HashMap::new()));
223
224        let pending_d = Arc::clone(&pending_diag);
225        let counts = Arc::clone(&diagnostic_counts);
226
227        let diagnostics_arc = Arc::clone(&self.diagnostics);
228        stderr_dispatcher.add_callback(
229            r"^(?P<level>\w+)(\[(?P<error_code>E\d+)\])?:\s+(?P<msg>.+)$", // Regex for diagnostic line
230            Box::new(move |_line, caps, _multiline_flag, _stats| {
231                if let Some(caps) = caps {
232                    let mut counts = counts.lock().unwrap();
233                    // Create a PendingDiag and save the message
234                    let mut pending_diag = pending_d.lock().unwrap();
235                    let mut last_lineref = String::new();
236                    if let Some(existing_diag) = pending_diag.take() {
237                        let mut diags = diagnostics_arc.lock().unwrap();
238                        last_lineref = existing_diag.lineref.clone();
239                        //                 let diag_level = match existing_diag.level.as_str() {
240                        //     "error" => CargoDiagnosticLevel::Error,
241                        //     "warning" => CargoDiagnosticLevel::Warning,
242                        //     "help" => CargoDiagnosticLevel::Help,
243                        //     "note" => CargoDiagnosticLevel::Note,
244                        //     _ => {
245                        //         println!("Unknown diagnostic level: {}", existing_diag.level);
246                        //         return None; // Ignore unknown levels
247                        //     }
248                        // };
249                        // *counts.entry(diag_level).or_insert(0) += 1;
250                        // let current_count = counts.get(&diag_level).unwrap_or(&0);
251                        // existing_diag.diag_number = Some(*current_count);
252                        //println!("{:?}", existing_diag);
253                        diags.push(existing_diag.clone());
254                    }
255                    log::trace!("Diagnostic line: {}", _line);
256                    let level = caps["level"].to_string(); // e.g., "warning", "error"
257                                                           // If the diagnostic level is "note" or "help", skip processing.
258                                                           // if level == "note" || level == "help" {
259                                                           //     println!("Skipping note/help diagnostic: {}", _line);
260                                                           //     return None;
261                                                           // }
262                    let message = caps["msg"].to_string();
263                    // If the message contains "generated" followed by one or more digits,
264                    // then ignore this diagnostic by returning None.
265                    //
266                    // Using a regex to check for "generated\s+\d+"
267                    let re_generated = regex::Regex::new(r"generated\s+\d+").unwrap();
268                    if re_generated.is_match(&message) {
269                        log::trace!("Skipping generated diagnostic: {}", _line);
270                        return None;
271                    }
272
273                    let error_code = caps.name("error_code").map(|m| m.as_str().to_string());
274                    let diag_level = match level.as_str() {
275                        "error" => CargoDiagnosticLevel::Error,
276                        "warning" => CargoDiagnosticLevel::Warning,
277                        "help" => CargoDiagnosticLevel::Help,
278                        "note" => CargoDiagnosticLevel::Note,
279                        _ => {
280                            println!("Unknown diagnostic level: {}", level);
281                            return None; // Ignore unknown levels
282                        }
283                    };
284                    // Increment the count for this level
285                    *counts.entry(diag_level).or_insert(0) += 1;
286                    // // Check if the previous diagnostic
287
288                    let current_count = counts.get(&diag_level).unwrap_or(&0);
289                    let diag = CargoDiagnostic {
290                        error_code: error_code.clone(),
291                        lineref: last_lineref.clone(),
292                        level: level.clone(),
293                        message,
294                        suggestion: None,
295                        help: None,
296                        note: None,
297                        uses_color: true,
298                        diag_num_padding: Some(2),
299                        diag_number: Some(*current_count),
300                    };
301
302                    // // Check if the previous diagnostic has a callback response
303                    // if let Some(existing_diag) = pending_diag.take() {
304                    //     // Save the current diagnostic's callback response
305                    //     if let Some(response) = existing_diag.callback_response {
306                    //         // Save it into the new diagnostic
307                    //         diag.save_callback_response(response);
308                    //     }
309                    // }
310
311                    // Save the new diagnostic
312                    *pending_diag = Some(diag);
313
314                    // Track the count of diagnostics for each level
315                    return Some(CallbackResponse {
316                        callback_type: CallbackType::LevelMessage, // Treat subsequent lines as warnings
317                        message: None,
318                        file: None,
319                        line: None,
320                        column: None,
321                        suggestion: None, // This is the suggestion part
322                        terminal_status: None,
323                    });
324                } else {
325                    println!("No captures found in line: {}", _line);
326                    None
327                }
328            }),
329        );
330
331        {
332            let location_lock_clone = Arc::clone(&warning_location);
333            let suggestion_m = Arc::clone(&suggestion_mode);
334
335            // Suggestion callback that adds subsequent lines as suggestions
336            stderr_dispatcher.add_callback(
337                r"^(?P<msg>.*)$", // Capture all lines following the location
338                Box::new(move |line, _captures, _multiline_flag, _stats| {
339                    if suggestion_m.load(Ordering::Relaxed) {
340                        // Only process lines that match the suggestion format
341                        if let Some(caps) = suggestion_regex.captures(line.trim()) {
342                            // Capture the line number and code from the suggestion line
343                            // let line_num = caps[1].parse::<usize>().unwrap_or(0);
344                            let code = caps[2].to_string();
345
346                            // Lock the pending_diag to add the suggestion
347                            if let Ok(mut lock) = location_lock_clone.lock() {
348                                if let Some(mut loc) = lock.take() {
349                                    // let file = loc.file.clone().unwrap_or_default();
350                                    // let col = loc.column.unwrap_or(0);
351
352                                    // Concatenate the suggestion line to the message
353                                    let mut msg = loc.message.unwrap_or_default();
354                                    msg.push_str(&format!("\n{}", code));
355
356                                    // Print the concatenated suggestion for debugging
357                                    // println!("daveSuggestion for {}:{}:{} - {}", file, line_num, col, msg);
358
359                                    // Update the location with the new concatenated message
360                                    loc.message = Some(msg.clone());
361                                    // println!("Updating location lock with new suggestion: {}", msg);
362                                    // Save the updated location back to shared state
363                                    // if let Ok(mut lock) = location_lock_clone.lock() {
364                                    // println!("Updating location lock with new suggestion: {}", msg);
365                                    lock.replace(loc);
366                                    // } else {
367                                    //     eprintln!("Failed to acquire lock for location_lock_clone");
368                                    // }
369                                }
370                                // return Some(CallbackResponse {
371                                //     callback_type: CallbackType::Warning, // Treat subsequent lines as warnings
372                                //     message: Some(msg.clone()),
373                                //     file: Some(file),
374                                //     line: Some(line_num),
375                                //     column: Some(col),
376                                //     suggestion: Some(msg),  // This is the suggestion part
377                                //     terminal_status: None,
378                                // });
379                            }
380                        }
381                    } else {
382                        // println!("Suggestion mode is not active. Ignoring line: {}", line);
383                    }
384
385                    None
386                }),
387            );
388        }
389        {
390            let suggestion_m = Arc::clone(&suggestion_mode);
391            let pending_diag_clone = Arc::clone(&pending_diag);
392            let diagnostics_arc = Arc::clone(&self.diagnostics);
393            // Callback for handling when an empty line or new diagnostic is received
394            stderr_dispatcher.add_callback(
395                r"^\s*$", // Regex to capture empty line
396                Box::new(move |_line, _captures, _multiline_flag, _stats| {
397                    // println!("Empty line detected: {}", line);
398                    suggestion_m.store(false, Ordering::Relaxed);
399                    // End of current diagnostic: take and process it.
400                    if let Some(pending_diag) = pending_diag_clone.lock().unwrap().take() {
401                        //println!("{:?}", pending_diag);
402                        // Use diagnostics_arc instead of self.diagnostices
403                        let mut diags = diagnostics_arc.lock().unwrap();
404                        diags.push(pending_diag.clone());
405                    } else {
406                        // println!("No pending diagnostic to process.");
407                    }
408                    // Handle empty line scenario to end the current diagnostic processing
409                    // if let Some(pending_diag) = pending_diag_clone.lock().unwrap().take() {
410                    //     println!("{:?}", pending_diag);
411                    //     let mut diags = self.diagnostics.lock().unwrap();
412                    //     diags.push(pending_diag.clone());
413                    //                             // let diag = crate::e_eventdispatcher::convert_message_to_diagnostic(msg, &msg_str);
414                    //                             // diags.push(diag.clone());
415                    //                             // if let Some(ref sd) = stage_disp_clone {
416                    //                             //     sd.dispatch(&format!("Stage: Diagnostic occurred at {:?}", now));
417                    //                             // }
418                    //     // Handle the saved PendingDiag and its CallbackResponse
419                    //     // if let Some(callback_response) = pending_diag.callback_response {
420                    //     //     println!("End of Diagnostic: {:?}", callback_response);
421                    //     // }
422                    // } else {
423                    //     println!("No pending diagnostic to process.");
424                    // }
425
426                    None
427                }),
428            );
429        }
430
431        // {
432        //     let pending_diag = Arc::clone(&pending_diag);
433        //     let location_lock = Arc::clone(&warning_location);
434        //     let suggestion_m = Arc::clone(&suggestion_mode);
435
436        // let suggestion_regex = Regex::new(r"^\s*(\d+)\s*\|\s*(.*)$").unwrap();
437
438        //     stderr_dispatcher.add_callback(
439        //     r"^\s*(\d+)\s*\|\s*(.*)$",  // Match suggestion line format
440        //     Box::new(move |line, _captures, _multiline_flag| {
441        //         if suggestion_m.load(Ordering::Relaxed) {
442        //             // Only process lines that match the suggestion format
443        //             if let Some(caps) = suggestion_regex.captures(line.trim()) {
444        //                 // Capture the line number and code from the suggestion line
445        //                 let line_num = caps[1].parse::<usize>().unwrap_or(0);
446        //                 let code = caps[2].to_string();
447
448        //                 // Lock the pending_diag to add the suggestion
449        //                 if let Some(mut loc) = location_lock.lock().unwrap().take() {
450        //                     println!("Suggestion line: {}", line);
451        //                     let file = loc.file.clone().unwrap_or_default();
452        //                     let col = loc.column.unwrap_or(0);
453
454        //                     // Concatenate the suggestion line to the message
455        //                     let mut msg = loc.message.unwrap_or_default();
456        //                     msg.push_str(&format!("\n{} | {}", line_num, code));  // Append the suggestion properly
457
458        //                     // Print the concatenated suggestion for debugging
459        //                     println!("Suggestion for {}:{}:{} - {}", file, line_num, col, msg);
460
461        //                     // Update the location with the new concatenated message
462        //                     loc.message = Some(msg.clone());
463
464        //                     // Save the updated location back to shared state
465        //                     location_lock.lock().unwrap().replace(loc);
466
467        //                     // return Some(CallbackResponse {
468        //                     //     callback_type: CallbackType::Warning, // Treat subsequent lines as warnings
469        //                     //     message: Some(msg.clone()),
470        //                     //     file: Some(file),
471        //                     //     line: Some(line_num),
472        //                     //     column: Some(col),
473        //                     //     suggestion: Some(msg),  // This is the suggestion part
474        //                     //     terminal_status: None,
475        //                     // });
476        //                 } else {
477        //                     println!("No location information available for suggestion line: {}", line);
478        //                 }
479        //             } else {
480        //                 println!("Suggestion line does not match expected format: {}", line);
481        //             }
482        //         } else {
483        //             println!("Suggestion mode is not active. Ignoring line: {}", line);
484        //         }
485
486        //         None
487        //     }),
488        // );
489
490        // }
491
492        {
493            let location_lock = Arc::clone(&warning_location);
494            let pending_diag = Arc::clone(&pending_diag);
495            let suggestion_mode = Arc::clone(&suggestion_mode);
496            stderr_dispatcher.add_callback(
497                r"^(?P<msg>.*)$", // Capture all lines following the location
498                Box::new(move |line, _captures, _multiline_flag, _stats| {
499                    // Lock the location to fetch the original diagnostic info
500                    if let Ok(location_guard) = location_lock.lock() {
501                        if let Some(loc) = location_guard.as_ref() {
502                            let file = loc.file.clone().unwrap_or_default();
503                            let line_num = loc.line.unwrap_or(0);
504                            let col = loc.column.unwrap_or(0);
505                            // println!("SUGGESTION: Suggestion for {}:{}:{} {}", file, line_num, col, line);
506
507                            // Only treat lines starting with | or numbers as suggestion lines
508                            if line.trim().starts_with('|')
509                                || line.trim().starts_with(char::is_numeric)
510                            {
511                                // Get the existing suggestion and append the new line
512                                let suggestion = line.trim();
513
514                                // Print the suggestion for debugging
515                                // println!("Suggestion for {}:{}:{} - {}", file, line_num, col, suggestion);
516
517                                // Lock the pending_diag and update its callback_response field
518                                let mut pending_diag = match pending_diag.lock() {
519                                    Ok(lock) => lock,
520                                    Err(e) => {
521                                        eprintln!("Failed to acquire lock: {}", e);
522                                        return None; // Handle the error appropriately
523                                    }
524                                };
525                                if let Some(diag) = pending_diag.take() {
526                                    // If a PendingDiag already exists, update the existing callback response with the new suggestion
527                                    let mut diag = diag;
528
529                                    // Append the new suggestion to the existing one
530                                    if let Some(ref mut existing) = diag.suggestion {
531                                        diag.suggestion =
532                                            Some(format!("{}\n{}", existing, suggestion));
533                                    } else {
534                                        diag.suggestion = Some(suggestion.to_string());
535                                    }
536
537                                    // Update the shared state with the new PendingDiag
538                                    *pending_diag = Some(diag.clone());
539                                    return Some(CallbackResponse {
540                                        callback_type: CallbackType::Suggestion, // Treat subsequent lines as warnings
541                                        message: Some(
542                                            diag.clone().suggestion.clone().unwrap().clone(),
543                                        ),
544                                        file: Some(file),
545                                        line: Some(line_num),
546                                        column: Some(col),
547                                        suggestion: diag.clone().suggestion.clone(), // This is the suggestion part
548                                        terminal_status: None,
549                                    });
550                                } else {
551                                    // println!("No pending diagnostic to process for suggestion line: {}", line);
552                                }
553                            } else {
554                                // If the line doesn't match the suggestion format, just return it as is
555                                if line.trim().is_empty() {
556                                    // Ignore empty lines
557                                    suggestion_mode.store(false, Ordering::Relaxed);
558                                    return None;
559                                }
560                            }
561                        } else {
562                            // println!("No location information available for suggestion line: {}", line);
563                        }
564                    }
565                    None
566                }),
567            );
568        }
569
570        // 2) Location callback stores its response into that shared state
571        {
572            let pending_diag = Arc::clone(&pending_diag);
573            let warning_location = Arc::clone(&warning_location);
574            let location_lock = Arc::clone(&warning_location);
575            let suggestion_mode = Arc::clone(&suggestion_mode);
576            let manifest_path = self.manifest_path.clone();
577            stderr_dispatcher.add_callback(
578                // r"^\s*-->\s+(?P<file>[^:]+):(?P<line>\d+):(?P<col>\d+)$",
579                r"^\s*-->\s+(?P<file>.+?)(?::(?P<line>\d+))?(?::(?P<col>\d+))?\s*$",
580                Box::new(move |_line, caps, _multiline_flag, _stats| {
581                    log::trace!("Location line: {}", _line);
582                    // if multiline_flag.load(Ordering::Relaxed) {
583                    if let Some(caps) = caps {
584                        let file = caps["file"].to_string();
585                        let resolved_path = resolve_file_path(&manifest_path, &file);
586                        let file = resolved_path.to_str().unwrap_or_default().to_string();
587                        let line = caps["line"].parse::<usize>().unwrap_or(0);
588                        let column = caps["col"].parse::<usize>().unwrap_or(0);
589                        let resp = CallbackResponse {
590                            callback_type: CallbackType::Location,
591                            message: format!("{}:{}:{}", file, line, column).into(),
592                            file: Some(file.clone()),
593                            line: Some(line),
594                            column: Some(column),
595                            suggestion: None,
596                            terminal_status: None,
597                        };
598                        // Lock the pending_diag and update its callback_response field
599                        let mut pending_diag = pending_diag.lock().unwrap();
600                        if let Some(diag) = pending_diag.take() {
601                            // If a PendingDiag already exists, save the new callback response in the existing PendingDiag
602                            let mut diag = diag;
603                            diag.lineref = format!("{}:{}:{}", file, line, column); // Update the lineref
604                                                                                    // diag.save_callback_response(resp.clone()); // Save the callback response
605                                                                                    // Update the shared state with the new PendingDiag
606                            *pending_diag = Some(diag);
607                        }
608                        // Save it for the generic callback to see
609                        *warning_location.lock().unwrap() = Some(resp.clone());
610                        *location_lock.lock().unwrap() = Some(resp.clone());
611                        // Set suggestion mode to true as we've encountered a location line
612                        suggestion_mode.store(true, Ordering::Relaxed);
613                        return Some(resp.clone());
614                    } else {
615                        println!("No captures found in line: {}", _line);
616                    }
617                    // }
618                    None
619                }),
620            );
621        }
622
623        // // 3) Note callback — attach note to pending_diag
624        {
625            let pending_diag = Arc::clone(&pending_diag);
626            stderr_dispatcher.add_callback(
627                r"^\s*=\s*note:\s*(?P<msg>.+)$",
628                Box::new(move |_line, caps, _state, _stats| {
629                    if let Some(caps) = caps {
630                        let mut pending_diag = pending_diag.lock().unwrap();
631                        if let Some(ref mut resp) = *pending_diag {
632                            // Prepare the new note with the blue prefix
633                            let new_note = format!("note: {}", caps["msg"].to_string());
634
635                            // Append or set the note
636                            if let Some(existing_note) = &resp.note {
637                                // If there's already a note, append with newline and the new note
638                                resp.note = Some(format!("{}\n{}", existing_note, new_note));
639                            } else {
640                                // If no existing note, just set the new note
641                                resp.note = Some(new_note);
642                            }
643                        }
644                    }
645                    None
646                }),
647            );
648        }
649
650        // 4) Help callback — attach help to pending_diag
651        {
652            let pending_diag = Arc::clone(&pending_diag);
653            stderr_dispatcher.add_callback(
654                r"^\s*(?:\=|\|)\s*help:\s*(?P<msg>.+)$", // Regex to match both '=' and '|' before help:
655                Box::new(move |_line, caps, _state, _stats| {
656                    if let Some(caps) = caps {
657                        let mut pending_diag = pending_diag.lock().unwrap();
658                        if let Some(ref mut resp) = *pending_diag {
659                            // Create the new help message with the orange "h:" prefix
660                            let new_help =
661                                format!("\x1b[38;5;214mhelp: {}\x1b[0m", caps["msg"].to_string());
662
663                            // Append or set the help message
664                            if let Some(existing_help) = &resp.help {
665                                // If there's already a help message, append with newline
666                                resp.help = Some(format!("{}\n{}", existing_help, new_help));
667                            } else {
668                                // If no existing help message, just set the new one
669                                resp.help = Some(new_help);
670                            }
671                        }
672                    }
673                    None
674                }),
675            );
676        }
677
678        stderr_dispatcher.add_callback(
679    r"(?:\x1b\[[0-9;]*[A-Za-z])*\s*Serving(?:\x1b\[[0-9;]*[A-Za-z])*\s+at\s+(http://[^\s]+)",
680    Box::new(|line, captures, _state, stats | {
681        if let Some(caps) = captures {
682            let url = caps.get(1).unwrap().as_str();
683            let url = url.replace("0.0.0.0", "127.0.0.1");
684            println!("(STDERR) Captured URL: {}", url);
685            match open::that_detached(&url) {
686                Ok(_) => println!("(STDERR) Opened URL: {}",&url),
687                Err(e) => eprintln!("(STDERR) Failed to open URL: {}. Error: {:?}", url, e),
688            }
689             let mut stats = stats.lock().unwrap();
690             if stats.build_finished_time.is_none() {
691              let now = SystemTime::now();
692             stats.build_finished_time = Some(now);
693             }
694            Some(CallbackResponse {
695                callback_type: CallbackType::OpenedUrl, // Choose as appropriate
696                message: Some(format!("Captured and opened URL: {}", url)),
697                file: None,
698                line: None,
699                column: None,
700                suggestion: None,
701                terminal_status: None,
702            })
703        } else {
704            println!("(STDERR) No URL captured in line: {}", line);
705            None
706        }
707    }),
708);
709
710        let finished_flag = Arc::new(AtomicBool::new(false));
711
712        // 0) Finished‐profile summary callback
713        {
714            let finished_flag = Arc::clone(&finished_flag);
715            stderr_dispatcher.add_callback(
716        r"^Finished\s+`(?P<profile>[^`]+)`\s+profile\s+\[(?P<opts>[^\]]+)\]\s+target\(s\)\s+in\s+(?P<dur>[0-9.]+s)$",
717        Box::new(move |_line, caps, _multiline_flag, stats | {
718            if let Some(caps) = caps {
719                finished_flag.store(true, Ordering::Relaxed);
720                let profile = &caps["profile"];
721                let opts    = &caps["opts"];
722                let dur     = &caps["dur"];
723                             let mut stats = stats.lock().unwrap();
724             if stats.build_finished_time.is_none() {
725              let now = SystemTime::now();
726             stats.build_finished_time = Some(now);
727             }
728                Some(CallbackResponse {
729                    callback_type: CallbackType::Note,
730                    message: Some(format!("Finished `{}` [{}] in {}", profile, opts, dur)),
731                    file: None, line: None, column: None, suggestion: None, terminal_status: None,
732                })
733            } else {
734                None
735            }
736        }),
737    );
738        }
739
740        let summary_flag = Arc::new(AtomicBool::new(false));
741        {
742            let summary_flag = Arc::clone(&summary_flag);
743            stderr_dispatcher.add_callback(
744    r"^(?P<level>warning|error):\s+`(?P<name>[^`]+)`\s+\((?P<otype>lib|bin)\)\s+generated\s+(?P<count>\d+)\s+(?P<kind>warnings|errors).*run\s+`(?P<cmd>[^`]+)`\s+to apply\s+(?P<fixes>\d+)\s+suggestions",
745    Box::new(move |_line, caps, multiline_flag, _stats | {
746        let summary_flag = Arc::clone(&summary_flag);
747        if let Some(caps) = caps {
748            summary_flag.store(true, Ordering::Relaxed);
749            // Always start fresh
750            multiline_flag.store(false, Ordering::Relaxed);
751
752            let level    = &caps["level"];
753            let name     = &caps["name"];
754            let otype    = &caps["otype"];
755            let count: usize = caps["count"].parse().unwrap_or(0);
756            let kind     = &caps["kind"];   // "warnings" or "errors"
757            let cmd      = caps["cmd"].to_string();
758            let fixes: usize = caps["fixes"].parse().unwrap_or(0);
759
760            println!("SUMMARIZATION CALLBACK {}",
761                    &format!("{}: `{}` ({}) generated {} {}; run `{}` to apply {} fixes",
762                    level, name, otype, count, kind, cmd, fixes));
763            Some(CallbackResponse {
764                callback_type: CallbackType::Note,  // treat as informational
765                message: Some(format!(
766                    "{}: `{}` ({}) generated {} {}; run `{}` to apply {} fixes",
767                    level, name, otype, count, kind, cmd, fixes
768                )),
769                file: None,
770                line: None,
771                column: None,
772                suggestion: Some(cmd),
773                terminal_status: None,
774            })
775        } else {
776            None
777        }
778    }),
779    );
780        }
781
782        // {
783        //     let summary_flag = Arc::clone(&summary_flag);
784        //     let finished_flag = Arc::clone(&finished_flag);
785        //     let warning_location = Arc::clone(&warning_location);
786        //     // Warning callback for stdout.
787        //     stderr_dispatcher.add_callback(
788        //         r"^warning:\s+(?P<msg>.+)$",
789        //         Box::new(
790        //             move |line: &str, captures: Option<regex::Captures>, multiline_flag: Arc<AtomicBool>| {
791        //                             // If summary or finished just matched, skip
792        //             if summary_flag.swap(false, Ordering::Relaxed)
793        //                 || finished_flag.swap(false, Ordering::Relaxed)
794        //             {
795        //                 return None;
796        //             }
797
798        //         // 2) If this line *matches* the warning regex, handle as a new warning
799        //         if let Some(caps) = captures {
800        //             let msg = caps.name("msg").unwrap().as_str().to_string();
801        //                    // 1) If a location was saved, print file:line:col – msg
802        //             // println!("*WARNING detected: {:?}", msg);
803        //                 multiline_flag.store(true, Ordering::Relaxed);
804        //         if let Some(loc) = warning_location.lock().unwrap().take() {
805        //                 let file = loc.file.unwrap_or_default();
806        //                 let line_num = loc.line.unwrap_or(0);
807        //                 let col  = loc.column.unwrap_or(0);
808        //                 println!("{}:{}:{} - {}", file, line_num, col, msg);
809        //                 return Some(CallbackResponse {
810        //                     callback_type: CallbackType::Warning,
811        //                     message: Some(msg.to_string()),
812        //                     file: None, line: None, column: None, suggestion: None, terminal_status: None,
813        //                 });
814        //         }
815        //             return Some(CallbackResponse {
816        //                 callback_type: CallbackType::Warning,
817        //                 message: Some(msg),
818        //                 file: None,
819        //                 line: None,
820        //                 column: None,
821        //                 suggestion: None,
822        //                 terminal_status: None,
823        //             });
824        //         }
825
826        //                 // 3) Otherwise, if we’re in multiline mode, treat as continuation
827        //         if multiline_flag.load(Ordering::Relaxed) {
828        //             let text = line.trim();
829        //             if text.is_empty() {
830        //                 multiline_flag.store(false, Ordering::Relaxed);
831        //                 return None;
832        //             }
833        //             // println!("   - {:?}", text);
834        //             return Some(CallbackResponse {
835        //                 callback_type: CallbackType::Warning,
836        //                 message: Some(text.to_string()),
837        //                 file: None,
838        //                 line: None,
839        //                 column: None,
840        //                 suggestion: None,
841        //                 terminal_status: None,
842        //             });
843        //         }
844        //                     None
845        //             },
846        //         ),
847        //     );
848        // }
849
850        stderr_dispatcher.add_callback(
851            r"IO\(Custom \{ kind: NotConnected",
852            Box::new(move |line, _captures, _state, _stats| {
853                println!("(STDERR) Terminal error detected: {:?}", &line);
854                let result = if line.contains("NotConnected") {
855                    TerminalError::NoTerminal
856                } else {
857                    TerminalError::NoError
858                };
859                let sender = sender.lock().unwrap();
860                sender.send(result).ok();
861                Some(CallbackResponse {
862                    callback_type: CallbackType::Warning, // Choose as appropriate
863                    message: Some(format!("Terminal Error: {}", line)),
864                    file: None,
865                    line: None,
866                    column: None,
867                    suggestion: None,
868                    terminal_status: None,
869                })
870            }),
871        );
872        stderr_dispatcher.add_callback(
873            r".*",
874            Box::new(|line, _captures, _state, _stats| {
875                log::trace!("stdraw[{:?}]", line);
876                None // We're just printing, so no callback response is needed.
877            }),
878        );
879        // need to implement autosense/filtering for tool installers; TBD
880        // stderr_dispatcher.add_callback(
881        //     r"Command 'perl' not found\. Is perl installed\?",
882        //     Box::new(|line, _captures, _state, stats| {
883        //     println!("cargo e sees a perl issue; maybe a prompt in the future or auto-resolution.");
884        //     crate::e_autosense::auto_sense_perl();
885        //     None
886        //     }),
887        // );
888        // need to implement autosense/filtering for tool installers; TBD
889        // stderr_dispatcher.add_callback(
890        //     r"Error configuring OpenSSL build:\s+Command 'perl' not found\. Is perl installed\?",
891        //     Box::new(|line, _captures, _state, stats| {
892        //     println!("Detected OpenSSL build error due to missing 'perl'. Attempting auto-resolution.");
893        //     crate::e_autosense::auto_sense_perl();
894        //     None
895        //     }),
896        // );
897        self.stderr_dispatcher = Some(Arc::new(stderr_dispatcher));
898
899        // let mut progress_dispatcher = EventDispatcher::new();
900        // progress_dispatcher.add_callback(r"Progress", Box::new(|line, _captures,_state| {
901        //     println!("(Progress) {}", line);
902        //     None
903        // }));
904        // self.progress_dispatcher = Some(Arc::new(progress_dispatcher));
905
906        // let mut stage_dispatcher = EventDispatcher::new();
907        // stage_dispatcher.add_callback(r"Stage:", Box::new(|line, _captures, _state| {
908        //     println!("(Stage) {}", line);
909        //     None
910        // }));
911        // self.stage_dispatcher = Some(Arc::new(stage_dispatcher));
912    }
913
914    pub fn run<F>(self: Arc<Self>, on_spawn: F) -> anyhow::Result<u32>
915    where
916        F: FnOnce(u32, CargoProcessHandle),
917    {
918        if !self.is_filter {
919            return self.switch_to_passthrough_mode(on_spawn);
920        }
921        let mut command = self.build_command();
922
923        let mut cargo_process_handle = command.spawn_cargo_capture(
924            self.clone(),
925            self.stdout_dispatcher.clone(),
926            self.stderr_dispatcher.clone(),
927            self.progress_dispatcher.clone(),
928            self.stage_dispatcher.clone(),
929            None,
930        );
931        cargo_process_handle.diagnostics = Arc::clone(&self.diagnostics);
932        let pid = cargo_process_handle.pid;
933
934        // Notify observer
935        on_spawn(pid, cargo_process_handle);
936
937        Ok(pid)
938    }
939
940    // pub fn run(self: Arc<Self>) -> anyhow::Result<u32> {
941    //     // Build the command using the builder's configuration
942    //     let mut command = self.build_command();
943
944    //     // Spawn the cargo process handle
945    //     let cargo_process_handle = command.spawn_cargo_capture(
946    //         self.stdout_dispatcher.clone(),
947    //         self.stderr_dispatcher.clone(),
948    //         self.progress_dispatcher.clone(),
949    //         self.stage_dispatcher.clone(),
950    //         None,
951    //     );
952    // let pid = cargo_process_handle.pid;
953    // let mut global = GLOBAL_CHILDREN.lock().unwrap();
954    // global.insert(pid, Arc::new(Mutex::new(cargo_process_handle)));
955    //     Ok(pid)
956    // }
957
958    pub fn wait(self: Arc<Self>, pid: Option<u32>) -> anyhow::Result<CargoProcessResult> {
959        let mut global = GLOBAL_CHILDREN.lock().unwrap();
960        if let Some(pid) = pid {
961            // Lock the global list of processes and attempt to find the cargo process handle directly by pid
962            if let Some(cargo_process_handle) = global.get_mut(&pid) {
963                let mut cargo_process_handle = cargo_process_handle.lock().unwrap();
964
965                // Wait for the process to finish and retrieve the result
966                // println!("Waiting for process with PID: {}", pid);
967                // let result = cargo_process_handle.wait();
968                // println!("Process with PID {} finished", pid);
969                loop {
970                    println!("Waiting for process with PID: {}", pid);
971
972                    // Attempt to wait for the process, but don't block indefinitely
973                    let status = cargo_process_handle.child.try_wait()?;
974
975                    // If the status is `Some(status)`, the process has finished
976                    if let Some(status) = status {
977                        if status.code() == Some(101) {
978                            println!("Process with PID {} finished with cargo error", pid);
979                        }
980
981                        // Check the terminal error flag and update the result if there is an error
982                        if *cargo_process_handle.terminal_error_flag.lock().unwrap()
983                            != TerminalError::NoError
984                        {
985                            let terminal_error =
986                                *cargo_process_handle.terminal_error_flag.lock().unwrap();
987                            cargo_process_handle.result.terminal_error = Some(terminal_error);
988                        }
989
990                        let final_diagnostics = {
991                            let diag_lock = self.diagnostics.lock().unwrap();
992                            diag_lock.clone()
993                        };
994                        cargo_process_handle.result.diagnostics = final_diagnostics.clone();
995                        cargo_process_handle.result.exit_status = Some(status);
996                        cargo_process_handle.result.end_time = Some(SystemTime::now());
997                        cargo_process_handle.result.elapsed_time = Some(
998                            cargo_process_handle
999                                .result
1000                                .end_time
1001                                .unwrap()
1002                                .duration_since(cargo_process_handle.result.start_time.unwrap())
1003                                .unwrap(),
1004                        );
1005                        println!(
1006                            "Process with PID {} finished {:?} {}",
1007                            pid,
1008                            status,
1009                            final_diagnostics.len()
1010                        );
1011                        return Ok(cargo_process_handle.result.clone());
1012                        // return Ok(CargoProcessResult { exit_status: status, ..Default::default() });
1013                    }
1014
1015                    // Sleep briefly to yield control back to the system and avoid blocking
1016                    std::thread::sleep(std::time::Duration::from_secs(1));
1017                }
1018
1019                // Return the result
1020                // match result {
1021                //     Ok(res) => Ok(res),
1022                //     Err(e) => Err(anyhow::anyhow!("Failed to wait for cargo process: {}", e).into()),
1023                // }
1024            } else {
1025                Err(anyhow::anyhow!(
1026                    "Process handle with PID {} not found in GLOBAL_CHILDREN",
1027                    pid
1028                )
1029                .into())
1030            }
1031        } else {
1032            Err(anyhow::anyhow!("No PID provided for waiting on cargo process").into())
1033        }
1034    }
1035
1036    // pub fn run_wait(self: Arc<Self>) -> anyhow::Result<CargoProcessResult> {
1037    //     // Run the cargo command and get the process handle (non-blocking)
1038    //     let pid = self.clone().run()?; // adds to global list of processes
1039    //     let result = self.wait(Some(pid)); // Wait for the process to finish
1040    //     // Remove the completed process from GLOBAL_CHILDREN
1041    //     let mut global = GLOBAL_CHILDREN.lock().unwrap();
1042    //     global.remove(&pid);
1043
1044    //     result
1045    // }
1046
1047    // Runs the cargo command using the builder's configuration.
1048    // pub fn run(&self) -> anyhow::Result<CargoProcessResult> {
1049    //     // Build the command using the builder's configuration
1050    //     let mut command = self.build_command();
1051
1052    //     // Now use the `spawn_cargo_capture` extension to run the command
1053    //     let mut cargo_process_handle = command.spawn_cargo_capture(
1054    //         self.stdout_dispatcher.clone(),
1055    //         self.stderr_dispatcher.clone(),
1056    //         self.progress_dispatcher.clone(),
1057    //         self.stage_dispatcher.clone(),
1058    //         None,
1059    //     );
1060
1061    //     // Wait for the process to finish and retrieve the results
1062    //     cargo_process_handle.wait().context("Failed to execute cargo process")
1063    // }
1064
1065    /// Configure the command based on the target kind.
1066    pub fn with_target(mut self, target: &CargoTarget) -> Self {
1067        if let Some(origin) = target.origin.clone() {
1068            println!("Target origin: {:?}", origin);
1069        } else {
1070            println!("Target origin is not set");
1071        }
1072        match target.kind {
1073            TargetKind::Unknown | TargetKind::Plugin => {
1074                return self;
1075            }
1076            TargetKind::Bench => {
1077                // // To run benchmarks, use the "bench" command.
1078                //  let exe_path = match which("bench") {
1079                //     Ok(path) => path,
1080                //     Err(err) => {
1081                //         eprintln!("Error: 'trunk' not found in PATH: {}", err);
1082                //         return self;
1083                //     }
1084                // };
1085                // self.alternate_cmd = Some("bench".to_string())
1086                self.args.push("bench".into());
1087                self.args.push(target.name.clone());
1088            }
1089            TargetKind::Test => {
1090                self.args.push("test".into());
1091                // Pass the target's name as a filter to run specific tests.
1092                self.args.push(target.name.clone());
1093            }
1094            TargetKind::UnknownExample
1095            | TargetKind::UnknownExtendedExample
1096            | TargetKind::Example
1097            | TargetKind::ExtendedExample => {
1098                self.args.push(self.subcommand.clone());
1099                //self.args.push("--message-format=json".into());
1100                self.args.push("--example".into());
1101                self.args.push(target.name.clone());
1102                self.args.push("--manifest-path".into());
1103                self.args.push(
1104                    target
1105                        .manifest_path
1106                        .clone()
1107                        .to_str()
1108                        .unwrap_or_default()
1109                        .to_owned(),
1110                );
1111                self = self.with_required_features(&target.manifest_path, target);
1112            }
1113            TargetKind::UnknownBinary
1114            | TargetKind::UnknownExtendedBinary
1115            | TargetKind::Binary
1116            | TargetKind::ExtendedBinary => {
1117                self.args.push(self.subcommand.clone());
1118                self.args.push("--bin".into());
1119                self.args.push(target.name.clone());
1120                self.args.push("--manifest-path".into());
1121                self.args.push(
1122                    target
1123                        .manifest_path
1124                        .clone()
1125                        .to_str()
1126                        .unwrap_or_default()
1127                        .to_owned(),
1128                );
1129                self = self.with_required_features(&target.manifest_path, target);
1130            }
1131            TargetKind::Manifest => {
1132                self.suppressed_flags.insert("quiet".to_string());
1133                self.args.push(self.subcommand.clone());
1134                self.args.push("--manifest-path".into());
1135                self.args.push(
1136                    target
1137                        .manifest_path
1138                        .clone()
1139                        .to_str()
1140                        .unwrap_or_default()
1141                        .to_owned(),
1142                );
1143            }
1144            TargetKind::ManifestTauriExample => {
1145                self.suppressed_flags.insert("quiet".to_string());
1146                self.args.push(self.subcommand.clone());
1147                self.args.push("--example".into());
1148                self.args.push(target.name.clone());
1149                self.args.push("--manifest-path".into());
1150                self.args.push(
1151                    target
1152                        .manifest_path
1153                        .clone()
1154                        .to_str()
1155                        .unwrap_or_default()
1156                        .to_owned(),
1157                );
1158                self = self.with_required_features(&target.manifest_path, target);
1159            }
1160            TargetKind::ScriptScriptisto => {
1161                let exe_path = match which("scriptisto") {
1162                    Ok(path) => path,
1163                    Err(err) => {
1164                        eprintln!("Error: 'scriptisto' not found in PATH: {}", err);
1165                        return self;
1166                    }
1167                };
1168                self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1169                let candidate_opt = match &target.origin {
1170                    Some(TargetOrigin::SingleFile(path))
1171                    | Some(TargetOrigin::DefaultBinary(path)) => Some(path),
1172                    _ => None,
1173                };
1174                if let Some(candidate) = candidate_opt {
1175                    self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1176                    self.args.push(candidate.to_string_lossy().to_string());
1177                } else {
1178                    println!("No scriptisto origin found for: {:?}", target);
1179                }
1180            }
1181            TargetKind::ScriptRustScript => {
1182                let exe_path = match crate::e_installer::ensure_rust_script() {
1183                    Ok(p) => p,
1184                    Err(e) => {
1185                        eprintln!("{}", e);
1186                        return self;
1187                    }
1188                };
1189                let candidate_opt = match &target.origin {
1190                    Some(TargetOrigin::SingleFile(path))
1191                    | Some(TargetOrigin::DefaultBinary(path)) => Some(path),
1192                    _ => None,
1193                };
1194                if let Some(candidate) = candidate_opt {
1195                    self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1196                    if self.is_filter {
1197                        self.args.push("-c".into()); // ask for cargo output
1198                    }
1199                    self.args.push(candidate.to_string_lossy().to_string());
1200                } else {
1201                    println!("No rust-script origin found for: {:?}", target);
1202                }
1203            }
1204            TargetKind::ManifestTauri => {
1205                // First, locate the Cargo.toml using the existing function
1206                let manifest_path = crate::locate_manifest(true).unwrap_or_else(|_| {
1207                    eprintln!("Error: Unable to locate Cargo.toml file.");
1208                    std::process::exit(1);
1209                });
1210
1211                // Now, get the workspace parent from the manifest directory
1212                let manifest_dir = Path::new(&manifest_path)
1213                    .parent()
1214                    .unwrap_or(Path::new(".."));
1215
1216                let pnpm =
1217                    crate::e_installer::check_pnpm_and_install(manifest_dir).unwrap_or_else(|_| {
1218                        eprintln!("Error: Unable to check pnpm dependencies.");
1219                        std::process::exit(1);
1220                    });
1221                // Ensure npm dependencies are handled at the workspace parent level
1222                if pnpm == PathBuf::new() {
1223                    crate::e_installer::check_npm_and_install(manifest_dir).unwrap_or_else(|_| {
1224                        eprintln!("Error: Unable to check npm dependencies.");
1225                        std::process::exit(1);
1226                    });
1227                }
1228
1229                self.suppressed_flags.insert("quiet".to_string());
1230                // Helper closure to check for tauri.conf.json in a directory.
1231                let has_tauri_conf = |dir: &Path| -> bool { dir.join("tauri.conf.json").exists() };
1232
1233                // Helper closure to check for tauri.conf.json and package.json in a directory.
1234                let has_file = |dir: &Path, filename: &str| -> bool { dir.join(filename).exists() };
1235                // Try candidate's parent (if origin is SingleFile or DefaultBinary).
1236                let candidate_dir_opt = match &target.origin {
1237                    Some(TargetOrigin::SingleFile(path))
1238                    | Some(TargetOrigin::DefaultBinary(path)) => path.parent(),
1239                    _ => None,
1240                };
1241
1242                if let Some(candidate_dir) = candidate_dir_opt {
1243                    if has_tauri_conf(candidate_dir) {
1244                        println!("Using candidate directory: {}", candidate_dir.display());
1245                        self.execution_dir = Some(candidate_dir.to_path_buf());
1246                    } else if let Some(manifest_parent) = target.manifest_path.parent() {
1247                        if has_tauri_conf(manifest_parent) {
1248                            println!("Using manifest parent: {}", manifest_parent.display());
1249                            self.execution_dir = Some(manifest_parent.to_path_buf());
1250                        } else if let Some(grandparent) = manifest_parent.parent() {
1251                            if has_tauri_conf(grandparent) {
1252                                println!("Using manifest grandparent: {}", grandparent.display());
1253                                self.execution_dir = Some(grandparent.to_path_buf());
1254                            } else {
1255                                println!("No tauri.conf.json found in candidate, manifest parent, or grandparent; defaulting to manifest parent: {}", manifest_parent.display());
1256                                self.execution_dir = Some(manifest_parent.to_path_buf());
1257                            }
1258                        } else {
1259                            println!("No grandparent for manifest; defaulting to candidate directory: {}", candidate_dir.display());
1260                            self.execution_dir = Some(candidate_dir.to_path_buf());
1261                        }
1262                    } else {
1263                        println!(
1264                            "No manifest parent found for: {}",
1265                            target.manifest_path.display()
1266                        );
1267                    }
1268                    // Check for package.json and run npm ls if found.
1269                    println!("Checking for package.json in: {}", candidate_dir.display());
1270                    if has_file(candidate_dir, "package.json") {
1271                        crate::e_installer::check_npm_and_install(candidate_dir).ok();
1272                    }
1273                } else if let Some(manifest_parent) = target.manifest_path.parent() {
1274                    if has_tauri_conf(manifest_parent) {
1275                        println!("Using manifest parent: {}", manifest_parent.display());
1276                        self.execution_dir = Some(manifest_parent.to_path_buf());
1277                    } else if let Some(grandparent) = manifest_parent.parent() {
1278                        if has_tauri_conf(grandparent) {
1279                            println!("Using manifest grandparent: {}", grandparent.display());
1280                            self.execution_dir = Some(grandparent.to_path_buf());
1281                        } else {
1282                            println!(
1283                                "No tauri.conf.json found; defaulting to manifest parent: {}",
1284                                manifest_parent.display()
1285                            );
1286                            self.execution_dir = Some(manifest_parent.to_path_buf());
1287                        }
1288                    }
1289                    // Check for package.json and run npm ls if found.
1290                    println!(
1291                        "Checking for package.json in: {}",
1292                        manifest_parent.display()
1293                    );
1294                    if has_file(manifest_parent, "package.json") {
1295                        crate::e_installer::check_npm_and_install(manifest_parent).ok();
1296                    }
1297                    if has_file(Path::new("."), "package.json") {
1298                        crate::e_installer::check_npm_and_install(manifest_parent).ok();
1299                    }
1300                } else {
1301                    println!(
1302                        "No manifest parent found for: {}",
1303                        target.manifest_path.display()
1304                    );
1305                }
1306                self.args.push("tauri".into());
1307                self.args.push("dev".into());
1308            }
1309            TargetKind::ManifestLeptos => {
1310                let readme_path = target
1311                    .manifest_path
1312                    .parent()
1313                    .map(|p| p.join("README.md"))
1314                    .filter(|p| p.exists())
1315                    .or_else(|| {
1316                        target
1317                            .manifest_path
1318                            .parent()
1319                            .map(|p| p.join("readme.md"))
1320                            .filter(|p| p.exists())
1321                    });
1322
1323                if let Some(readme) = readme_path {
1324                    if let Ok(mut file) = std::fs::File::open(&readme) {
1325                        let mut contents = String::new();
1326                        if file.read_to_string(&mut contents).is_ok()
1327                            && contents.contains("cargo leptos watch")
1328                        {
1329                            // Use cargo leptos watch
1330                            println!("Detected 'cargo leptos watch' in {}", readme.display());
1331                            crate::e_installer::ensure_leptos().unwrap_or_else(|_| {
1332                                eprintln!("Error: Unable to ensure leptos installation.");
1333                                PathBuf::new() // Return an empty PathBuf as a fallback
1334                            });
1335                            self.execution_dir =
1336                                target.manifest_path.parent().map(|p| p.to_path_buf());
1337
1338                            self.alternate_cmd = Some("cargo".to_string());
1339                            self.args.push("leptos".into());
1340                            self.args.push("watch".into());
1341                            self = self.with_required_features(&target.manifest_path, target);
1342                            if let Some(exec_dir) = &self.execution_dir {
1343                                if exec_dir.join("package.json").exists() {
1344                                    println!(
1345                                        "Found package.json in execution directory: {}",
1346                                        exec_dir.display()
1347                                    );
1348                                    crate::e_installer::check_npm_and_install(exec_dir).ok();
1349                                }
1350                            }
1351                            return self;
1352                        }
1353                    }
1354                }
1355
1356                // fallback to trunk
1357                let exe_path = match crate::e_installer::ensure_trunk() {
1358                    Ok(p) => p,
1359                    Err(e) => {
1360                        eprintln!("{}", e);
1361                        return self;
1362                    }
1363                };
1364
1365                if let Some(manifest_parent) = target.manifest_path.parent() {
1366                    println!("Manifest path: {}", target.manifest_path.display());
1367                    println!(
1368                        "Execution directory (same as manifest folder): {}",
1369                        manifest_parent.display()
1370                    );
1371                    self.execution_dir = Some(manifest_parent.to_path_buf());
1372                } else {
1373                    println!(
1374                        "No manifest parent found for: {}",
1375                        target.manifest_path.display()
1376                    );
1377                }
1378                if let Some(exec_dir) = &self.execution_dir {
1379                    if exec_dir.join("package.json").exists() {
1380                        println!(
1381                            "Found package.json in execution directory: {}",
1382                            exec_dir.display()
1383                        );
1384                        crate::e_installer::check_npm_and_install(exec_dir).ok();
1385                    }
1386                }
1387                self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1388                self.args.push("serve".into());
1389                self.args.push("--open".into());
1390                self.args.push("--color".into());
1391                self.args.push("always".into());
1392                self = self.with_required_features(&target.manifest_path, target);
1393            }
1394            TargetKind::ManifestDioxus => {
1395                // For Dioxus targets, print the manifest path and set the execution directory
1396                let exe_path = match crate::e_installer::ensure_dx() {
1397                    Ok(path) => path,
1398                    Err(e) => {
1399                        eprintln!("Error locating `dx`: {}", e);
1400                        return self;
1401                    }
1402                };
1403                // to be the same directory as the manifest.
1404                if let Some(manifest_parent) = target.manifest_path.parent() {
1405                    println!("Manifest path: {}", target.manifest_path.display());
1406                    println!(
1407                        "Execution directory (same as manifest folder): {}",
1408                        manifest_parent.display()
1409                    );
1410                    self.execution_dir = Some(manifest_parent.to_path_buf());
1411                } else {
1412                    println!(
1413                        "No manifest parent found for: {}",
1414                        target.manifest_path.display()
1415                    );
1416                }
1417                self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1418                self.args.push("serve".into());
1419                self = self.with_required_features(&target.manifest_path, target);
1420            }
1421            TargetKind::ManifestDioxusExample => {
1422                let exe_path = match crate::e_installer::ensure_dx() {
1423                    Ok(path) => path,
1424                    Err(e) => {
1425                        eprintln!("Error locating `dx`: {}", e);
1426                        return self;
1427                    }
1428                };
1429                // For Dioxus targets, print the manifest path and set the execution directory
1430                // to be the same directory as the manifest.
1431                if let Some(manifest_parent) = target.manifest_path.parent() {
1432                    println!("Manifest path: {}", target.manifest_path.display());
1433                    println!(
1434                        "Execution directory (same as manifest folder): {}",
1435                        manifest_parent.display()
1436                    );
1437                    self.execution_dir = Some(manifest_parent.to_path_buf());
1438                } else {
1439                    println!(
1440                        "No manifest parent found for: {}",
1441                        target.manifest_path.display()
1442                    );
1443                }
1444                self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1445                self.args.push("serve".into());
1446                self.args.push("--example".into());
1447                self.args.push(target.name.clone());
1448                self = self.with_required_features(&target.manifest_path, target);
1449            }
1450        }
1451        self
1452    }
1453
1454    /// Configure the command using CLI options.
1455    pub fn with_cli(mut self, cli: &crate::Cli) -> Self {
1456        if cli.quiet && !self.suppressed_flags.contains("quiet") {
1457            // Insert --quiet right after "run" if present.
1458            if let Some(pos) = self.args.iter().position(|arg| arg == &self.subcommand) {
1459                self.args.insert(pos + 1, "--quiet".into());
1460            } else {
1461                self.args.push("--quiet".into());
1462            }
1463        }
1464        if cli.release {
1465            // Insert --release right after the initial "run" command if applicable.
1466            // For example, if the command already contains "run", insert "--release" after it.
1467            if let Some(pos) = self.args.iter().position(|arg| arg == &self.subcommand) {
1468                self.args.insert(pos + 1, "--release".into());
1469            } else {
1470                // If not running a "run" command (like in the Tauri case), simply push it.
1471                self.args.push("--release".into());
1472            }
1473        }
1474        // Append extra arguments (if any) after a "--" separator.
1475        if !cli.extra.is_empty() {
1476            self.args.push("--".into());
1477            self.args.extend(cli.extra.iter().cloned());
1478        }
1479        self
1480    }
1481    /// Append required features based on the manifest, target kind, and name.
1482    /// This method queries your manifest helper function and, if features are found,
1483    /// appends "--features" and the feature list.
1484    pub fn with_required_features(mut self, manifest: &PathBuf, target: &CargoTarget) -> Self {
1485        if !self.args.contains(&"--features".to_string()) {
1486            if let Some(features) = crate::e_manifest::get_required_features_from_manifest(
1487            manifest,
1488            &target.kind,
1489            &target.name,
1490            ) {
1491            self.args.push("--features".to_string());
1492            self.args.push(features);
1493            }
1494        }
1495        self
1496    }
1497
1498    /// Appends extra arguments to the command.
1499    pub fn with_extra_args(mut self, extra: &[String]) -> Self {
1500        if !extra.is_empty() {
1501            // Use "--" to separate Cargo arguments from target-specific arguments.
1502            self.args.push("--".into());
1503            self.args.extend(extra.iter().cloned());
1504        }
1505        self
1506    }
1507
1508    /// Builds the final vector of command-line arguments.
1509    pub fn build(self) -> Vec<String> {
1510        self.args
1511    }
1512
1513    pub fn is_compiler_target(&self) -> bool {
1514        let supported_subcommands = ["run", "build", "check", "leptos", "tauri"];
1515        if let Some(alternate) = &self.alternate_cmd {
1516            if alternate == "trunk" {
1517                return true;
1518            }
1519            if alternate != "cargo" {
1520                return false;
1521            }
1522        }
1523        if let Some(_) = self
1524            .args
1525            .iter()
1526            .position(|arg| supported_subcommands.contains(&arg.as_str()))
1527        {
1528            return true;
1529        }
1530        false
1531    }
1532
1533    pub fn injected_args(&self) -> (String, Vec<String>) {
1534        let mut new_args = self.args.clone();
1535        let supported_subcommands = [
1536            "run", "build", "test", "bench", "clean", "doc", "publish", "update",
1537        ];
1538
1539        if self.is_filter {
1540            if let Some(pos) = new_args
1541                .iter()
1542                .position(|arg| supported_subcommands.contains(&arg.as_str()))
1543            {
1544                // If the command is a supported subcommand like "cargo run", insert the JSON output format and color options.
1545                new_args.insert(pos + 1, "--message-format=json".into());
1546                new_args.insert(pos + 2, "--color".into());
1547                new_args.insert(pos + 3, "always".into());
1548            }
1549        }
1550
1551        let program = self.alternate_cmd.as_deref().unwrap_or("cargo").to_string();
1552        (program, new_args)
1553    }
1554
1555    pub fn print_command(&self) {
1556        let (program, new_args) = self.injected_args();
1557        println!("{} {}", program, new_args.join(" "));
1558    }
1559
1560    /// builds a std::process::Command.
1561    pub fn build_command(&self) -> Command {
1562        let (program, new_args) = self.injected_args();
1563
1564        let mut cmd = Command::new(program);
1565        cmd.args(new_args);
1566
1567        if let Some(dir) = &self.execution_dir {
1568            cmd.current_dir(dir);
1569        }
1570
1571        cmd
1572    }
1573    /// Runs the command and returns everything it printed (stdout + stderr),
1574    /// regardless of exit status.
1575    pub fn capture_output(&self) -> anyhow::Result<String> {
1576        // Build and run
1577        let mut cmd = self.build_command();
1578        let output = cmd
1579            .output()
1580            .map_err(|e| anyhow::anyhow!("Failed to spawn cargo process: {}", e))?;
1581
1582        // Decode both stdout and stderr lossily
1583        let mut all = String::new();
1584        all.push_str(&String::from_utf8_lossy(&output.stdout));
1585        all.push_str(&String::from_utf8_lossy(&output.stderr));
1586
1587        // Return the combined string, even if exit was !success
1588        Ok(all)
1589    }
1590}
1591/// Resolves a file path by:
1592///   1. If the path is relative, try to resolve it relative to the current working directory.
1593///   2. If that file does not exist, try to resolve it relative to the parent directory of the manifest path.
1594///   3. Otherwise, return the original relative path.
1595fn resolve_file_path(manifest_path: &PathBuf, file_str: &str) -> PathBuf {
1596    let file_path = Path::new(file_str);
1597    if file_path.is_relative() {
1598        // 1. Try resolving relative to the current working directory.
1599        if let Ok(cwd) = env::current_dir() {
1600            let cwd_path = cwd.join(file_path);
1601            if cwd_path.exists() {
1602                return cwd_path;
1603            }
1604        }
1605        // 2. Try resolving relative to the parent of the manifest path.
1606        if let Some(manifest_parent) = manifest_path.parent() {
1607            let parent_path = manifest_parent.join(file_path);
1608            if parent_path.exists() {
1609                return parent_path;
1610            }
1611        }
1612        // 3. Neither existed; return the relative path as-is.
1613        return file_path.to_path_buf();
1614    }
1615    file_path.to_path_buf()
1616}
1617
1618// --- Example usage ---
1619#[cfg(test)]
1620mod tests {
1621    use crate::e_target::TargetOrigin;
1622
1623    use super::*;
1624
1625    #[test]
1626    fn test_command_builder_example() {
1627        let target_name = "my_example".to_string();
1628        let target = CargoTarget {
1629            name: "my_example".to_string(),
1630            display_name: "My Example".to_string(),
1631            manifest_path: "Cargo.toml".into(),
1632            kind: TargetKind::Example,
1633            extended: true,
1634            toml_specified: false,
1635            origin: Some(TargetOrigin::SingleFile(PathBuf::from(
1636                "examples/my_example.rs",
1637            ))),
1638        };
1639
1640        let extra_args = vec!["--flag".to_string(), "value".to_string()];
1641
1642        let manifest_path = PathBuf::from("Cargo.toml");
1643        let args = CargoCommandBuilder::new(&target_name, &manifest_path, "run", false)
1644            .with_target(&target)
1645            .with_extra_args(&extra_args)
1646            .build();
1647
1648        // For an example target, we expect something like:
1649        // cargo run --example my_example --manifest-path Cargo.toml -- --flag value
1650        assert!(args.contains(&"--example".to_string()));
1651        assert!(args.contains(&"my_example".to_string()));
1652        assert!(args.contains(&"--manifest-path".to_string()));
1653        assert!(args.contains(&"Cargo.toml".to_string()));
1654        assert!(args.contains(&"--".to_string()));
1655        assert!(args.contains(&"--flag".to_string()));
1656        assert!(args.contains(&"value".to_string()));
1657    }
1658}