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