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