1use crate::analyzer::{
7 MonorepoAnalysis, ProjectCategory, ArchitecturePattern,
8 DetectedTechnology, TechnologyCategory, LibraryType,
9 DockerAnalysis, OrchestrationPattern,
10};
11use colored::*;
12
13#[derive(Debug, Clone)]
15struct ContentLine {
16 label: String,
17 value: String,
18 label_colored: bool,
19}
20
21impl ContentLine {
22 fn new(label: &str, value: &str, label_colored: bool) -> Self {
23 Self {
24 label: label.to_string(),
25 value: value.to_string(),
26 label_colored,
27 }
28 }
29
30
31 fn separator() -> Self {
32 Self {
33 label: "SEPARATOR".to_string(),
34 value: String::new(),
35 label_colored: false,
36 }
37 }
38
39
40}
41
42pub struct BoxDrawer {
44 title: String,
45 lines: Vec<ContentLine>,
46 min_width: usize,
47 max_width: usize,
48}
49
50impl BoxDrawer {
51 pub fn new(title: &str) -> Self {
52 Self {
53 title: title.to_string(),
54 lines: Vec::new(),
55 min_width: 60,
56 max_width: 120, }
58 }
59
60 pub fn add_line(&mut self, label: &str, value: &str, label_colored: bool) {
61 self.lines.push(ContentLine::new(label, value, label_colored));
62 }
63
64 pub fn add_value_only(&mut self, value: &str) {
65 self.lines.push(ContentLine::new("", value, false));
66 }
67
68 pub fn add_separator(&mut self) {
69 self.lines.push(ContentLine::separator());
70 }
71
72 fn calculate_optimal_width(&self) -> usize {
74 let title_width = visual_width(&self.title) + 6; let mut max_content_width = 0;
76
77 for line in &self.lines {
79 if line.label == "SEPARATOR" {
80 continue;
81 }
82
83 let rendered_width = self.calculate_rendered_line_width(line);
84 max_content_width = max_content_width.max(rendered_width);
85 }
86
87 let content_width_with_buffer = max_content_width + 4; let needed_width = content_width_with_buffer + 4;
92
93 let optimal_width = title_width.max(needed_width).max(self.min_width);
95 optimal_width.min(self.max_width)
96 }
97
98 fn calculate_rendered_line_width(&self, line: &ContentLine) -> usize {
100 let label_width = visual_width(&line.label);
101 let value_width = visual_width(&line.value);
102
103 if !line.label.is_empty() && !line.value.is_empty() {
104 let min_label_space = if line.label_colored { 25 } else { label_width };
107 min_label_space + 2 + value_width } else if !line.value.is_empty() {
109 value_width
111 } else if !line.label.is_empty() {
112 label_width
114 } else {
115 0
117 }
118 }
119
120 pub fn draw(&self) -> String {
122 let box_width = self.calculate_optimal_width();
123 let content_width = box_width - 4; let mut output = Vec::new();
126
127 output.push(self.draw_top(box_width));
129
130 for line in &self.lines {
132 if line.label == "SEPARATOR" {
133 output.push(self.draw_separator(box_width));
134 } else if line.label.is_empty() && line.value.is_empty() {
135 output.push(self.draw_empty_line(box_width));
136 } else {
137 output.push(self.draw_content_line(line, content_width));
138 }
139 }
140
141 output.push(self.draw_bottom(box_width));
143
144 output.join("\n")
145 }
146
147 fn draw_top(&self, width: usize) -> String {
148 let title_colored = self.title.bright_cyan();
149 let title_len = visual_width(&self.title);
150
151 let prefix_len = 3; let suffix_len = 1; let title_space = 1; let remaining_space = width - prefix_len - title_len - title_space - suffix_len;
157
158 format!("┌─ {} {}┐",
159 title_colored,
160 "─".repeat(remaining_space)
161 )
162 }
163
164 fn draw_bottom(&self, width: usize) -> String {
165 format!("└{}┘", "─".repeat(width - 2))
166 }
167
168 fn draw_separator(&self, width: usize) -> String {
169 format!("│ {} │", "─".repeat(width - 4).dimmed())
170 }
171
172 fn draw_empty_line(&self, width: usize) -> String {
173 format!("│ {} │", " ".repeat(width - 4))
174 }
175
176 fn draw_content_line(&self, line: &ContentLine, content_width: usize) -> String {
177 let formatted_label = if line.label_colored && !line.label.is_empty() {
179 line.label.bright_white().to_string()
180 } else {
181 line.label.clone()
182 };
183
184 let label_display_width = visual_width(&line.label);
186 let value_display_width = visual_width(&line.value);
187
188 let content = if !line.label.is_empty() && !line.value.is_empty() {
190 let min_label_space = if line.label_colored { 25 } else { label_display_width };
192 let label_padding = min_label_space.saturating_sub(label_display_width);
193 let remaining_space = content_width.saturating_sub(min_label_space + 2); if value_display_width <= remaining_space {
196 let value_padding = remaining_space.saturating_sub(value_display_width);
198 format!("{}{:<width$} {}{}",
199 formatted_label,
200 "",
201 " ".repeat(value_padding),
202 line.value,
203 width = label_padding
204 )
205 } else {
206 let truncated_value = truncate_to_width(&line.value, remaining_space.saturating_sub(3));
208 format!("{}{:<width$} {}",
209 formatted_label,
210 "",
211 truncated_value,
212 width = label_padding
213 )
214 }
215 } else if !line.value.is_empty() {
216 if value_display_width <= content_width {
218 format!("{:<width$}", line.value, width = content_width)
219 } else {
220 truncate_to_width(&line.value, content_width)
221 }
222 } else if !line.label.is_empty() {
223 if label_display_width <= content_width {
225 format!("{:<width$}", formatted_label, width = content_width)
226 } else {
227 truncate_to_width(&formatted_label, content_width)
228 }
229 } else {
230 " ".repeat(content_width)
232 };
233
234 let actual_width = visual_width(&content);
236 let final_content = if actual_width < content_width {
237 format!("{}{}", content, " ".repeat(content_width - actual_width))
238 } else if actual_width > content_width {
239 truncate_to_width(&content, content_width)
240 } else {
241 content
242 };
243
244 format!("│ {} │", final_content)
245 }
246}
247
248fn visual_width(s: &str) -> usize {
250 let mut width = 0;
251 let mut chars = s.chars().peekable();
252
253 while let Some(ch) = chars.next() {
254 if ch == '\x1b' {
255 if chars.peek() == Some(&'[') {
257 chars.next(); while let Some(c) = chars.next() {
259 if c.is_ascii_alphabetic() {
260 break; }
262 }
263 }
264 } else {
265 width += char_width(ch);
268 }
269 }
270
271 width
272}
273
274fn char_width(ch: char) -> usize {
276 match ch {
277 '\u{0000}'..='\u{001F}' | '\u{007F}' => 0,
279 '\u{0300}'..='\u{036F}' => 0,
281 '\u{2600}'..='\u{26FF}' | '\u{2700}'..='\u{27BF}' | '\u{1F000}'..='\u{1F02F}' | '\u{1F030}'..='\u{1F09F}' | '\u{1F0A0}'..='\u{1F0FF}' | '\u{1F100}'..='\u{1F1FF}' | '\u{1F200}'..='\u{1F2FF}' | '\u{1F300}'..='\u{1F5FF}' | '\u{1F600}'..='\u{1F64F}' | '\u{1F650}'..='\u{1F67F}' | '\u{1F680}'..='\u{1F6FF}' | '\u{1F700}'..='\u{1F77F}' | '\u{1F780}'..='\u{1F7FF}' | '\u{1F800}'..='\u{1F8FF}' | '\u{1F900}'..='\u{1F9FF}' | '\u{1100}'..='\u{115F}' | '\u{2E80}'..='\u{2EFF}' | '\u{2F00}'..='\u{2FDF}' | '\u{2FF0}'..='\u{2FFF}' | '\u{3000}'..='\u{303E}' | '\u{3041}'..='\u{3096}' | '\u{30A1}'..='\u{30FA}' | '\u{3105}'..='\u{312D}' | '\u{3131}'..='\u{318E}' | '\u{3190}'..='\u{31BA}' | '\u{31C0}'..='\u{31E3}' | '\u{31F0}'..='\u{31FF}' | '\u{3200}'..='\u{32FF}' | '\u{3300}'..='\u{33FF}' | '\u{3400}'..='\u{4DBF}' | '\u{4E00}'..='\u{9FFF}' | '\u{A000}'..='\u{A48C}' | '\u{A490}'..='\u{A4C6}' | '\u{AC00}'..='\u{D7AF}' | '\u{F900}'..='\u{FAFF}' | '\u{FE10}'..='\u{FE19}' | '\u{FE30}'..='\u{FE6F}' | '\u{FF00}'..='\u{FF60}' | '\u{FFE0}'..='\u{FFE6}' => 2,
322 _ => 1,
324 }
325}
326
327fn truncate_to_width(s: &str, max_width: usize) -> String {
329 let current_visual_width = visual_width(s);
330 if current_visual_width <= max_width {
331 return s.to_string();
332 }
333
334 if s.contains('\x1b') {
336 let stripped = strip_ansi_codes(s);
338 if visual_width(&stripped) <= max_width {
339 return s.to_string();
340 }
341
342 let mut result = String::new();
344 let mut width = 0;
345 for ch in stripped.chars() {
346 let ch_width = char_width(ch);
347 if width + ch_width > max_width.saturating_sub(3) {
348 result.push_str("...");
349 break;
350 }
351 result.push(ch);
352 width += ch_width;
353 }
354 return result;
355 }
356
357 let mut result = String::new();
359 let mut width = 0;
360
361 for ch in s.chars() {
362 let ch_width = char_width(ch);
363 if width + ch_width > max_width.saturating_sub(3) {
364 result.push_str("...");
365 break;
366 }
367 result.push(ch);
368 width += ch_width;
369 }
370
371 result
372}
373
374fn strip_ansi_codes(s: &str) -> String {
376 let mut result = String::new();
377 let mut chars = s.chars().peekable();
378
379 while let Some(ch) = chars.next() {
380 if ch == '\x1b' {
381 if chars.peek() == Some(&'[') {
383 chars.next(); while let Some(c) = chars.next() {
385 if c.is_ascii_alphabetic() {
386 break; }
388 }
389 }
390 } else {
391 result.push(ch);
392 }
393 }
394
395 result
396}
397
398#[derive(Debug, Clone, Copy, PartialEq)]
400pub enum DisplayMode {
401 Matrix,
403 Detailed,
405 Summary,
407 Json,
409}
410
411pub fn display_analysis(analysis: &MonorepoAnalysis, mode: DisplayMode) {
413 match mode {
414 DisplayMode::Matrix => display_matrix_view(analysis),
415 DisplayMode::Detailed => display_detailed_view(analysis),
416 DisplayMode::Summary => display_summary_view(analysis),
417 DisplayMode::Json => display_json_view(analysis),
418 }
419}
420
421pub fn display_analysis_to_string(analysis: &MonorepoAnalysis, mode: DisplayMode) -> String {
423 match mode {
424 DisplayMode::Matrix => display_matrix_view_to_string(analysis),
425 DisplayMode::Detailed => display_detailed_view_to_string(analysis),
426 DisplayMode::Summary => display_summary_view_to_string(analysis),
427 DisplayMode::Json => display_json_view_to_string(analysis),
428 }
429}
430
431pub fn display_analysis_with_return(analysis: &MonorepoAnalysis, mode: DisplayMode) -> String {
433 let output = display_analysis_to_string(analysis, mode);
434 print!("{}", output);
435 output
436}
437
438pub fn display_matrix_view(analysis: &MonorepoAnalysis) {
440 println!("\n{}", "═".repeat(100).bright_blue());
442 println!("{}", "📊 PROJECT ANALYSIS DASHBOARD".bright_white().bold());
443 println!("{}", "═".repeat(100).bright_blue());
444
445 display_architecture_box(analysis);
447
448 display_technology_stack_box(analysis);
450
451 if analysis.projects.len() > 1 {
453 display_projects_matrix(analysis);
454 } else {
455 display_single_project_matrix(analysis);
456 }
457
458 if analysis.projects.iter().any(|p| p.analysis.docker_analysis.is_some()) {
460 display_docker_overview_matrix(analysis);
461 }
462
463 display_metrics_box(analysis);
465
466 println!("\n{}", "═".repeat(100).bright_blue());
468}
469
470pub fn display_matrix_view_to_string(analysis: &MonorepoAnalysis) -> String {
472 let mut output = String::new();
473
474 output.push_str(&format!("\n{}\n", "═".repeat(100).bright_blue()));
476 output.push_str(&format!("{}\n", "📊 PROJECT ANALYSIS DASHBOARD".bright_white().bold()));
477 output.push_str(&format!("{}\n", "═".repeat(100).bright_blue()));
478
479 output.push_str(&display_architecture_box_to_string(analysis));
481
482 output.push_str(&display_technology_stack_box_to_string(analysis));
484
485 if analysis.projects.len() > 1 {
487 output.push_str(&display_projects_matrix_to_string(analysis));
488 } else {
489 output.push_str(&display_single_project_matrix_to_string(analysis));
490 }
491
492 if analysis.projects.iter().any(|p| p.analysis.docker_analysis.is_some()) {
494 output.push_str(&display_docker_overview_matrix_to_string(analysis));
495 }
496
497 output.push_str(&display_metrics_box_to_string(analysis));
499
500 output.push_str(&format!("\n{}\n", "═".repeat(100).bright_blue()));
502
503 output
504}
505
506fn display_architecture_box(analysis: &MonorepoAnalysis) {
508 let mut box_drawer = BoxDrawer::new("Architecture Overview");
509
510 let arch_type = if analysis.is_monorepo {
511 format!("Monorepo ({} projects)", analysis.projects.len())
512 } else {
513 "Single Project".to_string()
514 };
515
516 box_drawer.add_line("Type:", &arch_type.yellow(), true);
517 box_drawer.add_line("Pattern:", &format!("{:?}", analysis.technology_summary.architecture_pattern).green(), true);
518
519 let pattern_desc = match &analysis.technology_summary.architecture_pattern {
521 ArchitecturePattern::Monolithic => "Single, self-contained application",
522 ArchitecturePattern::Fullstack => "Full-stack app with frontend/backend separation",
523 ArchitecturePattern::Microservices => "Multiple independent microservices",
524 ArchitecturePattern::ApiFirst => "API-first architecture with service interfaces",
525 ArchitecturePattern::EventDriven => "Event-driven with decoupled components",
526 ArchitecturePattern::Mixed => "Mixed architecture patterns",
527 };
528 box_drawer.add_value_only(&pattern_desc.dimmed());
529
530 println!("\n{}", box_drawer.draw());
531}
532
533fn display_architecture_box_to_string(analysis: &MonorepoAnalysis) -> String {
535 let mut box_drawer = BoxDrawer::new("Architecture Overview");
536
537 let arch_type = if analysis.is_monorepo {
538 format!("Monorepo ({} projects)", analysis.projects.len())
539 } else {
540 "Single Project".to_string()
541 };
542
543 box_drawer.add_line("Type:", &arch_type.yellow(), true);
544 box_drawer.add_line("Pattern:", &format!("{:?}", analysis.technology_summary.architecture_pattern).green(), true);
545
546 let pattern_desc = match &analysis.technology_summary.architecture_pattern {
548 ArchitecturePattern::Monolithic => "Single, self-contained application",
549 ArchitecturePattern::Fullstack => "Full-stack app with frontend/backend separation",
550 ArchitecturePattern::Microservices => "Multiple independent microservices",
551 ArchitecturePattern::ApiFirst => "API-first architecture with service interfaces",
552 ArchitecturePattern::EventDriven => "Event-driven with decoupled components",
553 ArchitecturePattern::Mixed => "Mixed architecture patterns",
554 };
555 box_drawer.add_value_only(&pattern_desc.dimmed());
556
557 format!("\n{}", box_drawer.draw())
558}
559
560fn display_technology_stack_box(analysis: &MonorepoAnalysis) {
562 let mut box_drawer = BoxDrawer::new("Technology Stack");
563
564 let mut has_content = false;
565
566 if !analysis.technology_summary.languages.is_empty() {
568 let languages = analysis.technology_summary.languages.join(", ");
569 box_drawer.add_line("Languages:", &languages.blue(), true);
570 has_content = true;
571 }
572
573 if !analysis.technology_summary.frameworks.is_empty() {
575 let frameworks = analysis.technology_summary.frameworks.join(", ");
576 box_drawer.add_line("Frameworks:", &frameworks.magenta(), true);
577 has_content = true;
578 }
579
580 if !analysis.technology_summary.databases.is_empty() {
582 let databases = analysis.technology_summary.databases.join(", ");
583 box_drawer.add_line("Databases:", &databases.cyan(), true);
584 has_content = true;
585 }
586
587 if !has_content {
588 box_drawer.add_value_only("No technologies detected");
589 }
590
591 println!("\n{}", box_drawer.draw());
592}
593
594fn display_technology_stack_box_to_string(analysis: &MonorepoAnalysis) -> String {
596 let mut box_drawer = BoxDrawer::new("Technology Stack");
597
598 let mut has_content = false;
599
600 if !analysis.technology_summary.languages.is_empty() {
602 let languages = analysis.technology_summary.languages.join(", ");
603 box_drawer.add_line("Languages:", &languages.blue(), true);
604 has_content = true;
605 }
606
607 if !analysis.technology_summary.frameworks.is_empty() {
609 let frameworks = analysis.technology_summary.frameworks.join(", ");
610 box_drawer.add_line("Frameworks:", &frameworks.magenta(), true);
611 has_content = true;
612 }
613
614 if !analysis.technology_summary.databases.is_empty() {
616 let databases = analysis.technology_summary.databases.join(", ");
617 box_drawer.add_line("Databases:", &databases.cyan(), true);
618 has_content = true;
619 }
620
621 if !has_content {
622 box_drawer.add_value_only("No technologies detected");
623 }
624
625 format!("\n{}", box_drawer.draw())
626}
627
628fn display_projects_matrix(analysis: &MonorepoAnalysis) {
630 let mut box_drawer = BoxDrawer::new("Projects Matrix");
631
632 let mut project_data = Vec::new();
634 for project in &analysis.projects {
635 let name = project.name.clone(); let proj_type = format_project_category(&project.project_category);
637
638 let languages = project.analysis.languages.iter()
639 .map(|l| l.name.clone())
640 .collect::<Vec<_>>()
641 .join(", ");
642
643 let main_tech = get_main_technologies(&project.analysis.technologies);
644
645 let ports = if project.analysis.ports.is_empty() {
646 "-".to_string()
647 } else {
648 project.analysis.ports.iter()
649 .map(|p| p.number.to_string())
650 .collect::<Vec<_>>()
651 .join(", ")
652 };
653
654 let docker = if project.analysis.docker_analysis.is_some() {
655 "Yes"
656 } else {
657 "No"
658 };
659
660 let deps_count = project.analysis.dependencies.len().to_string();
661
662 project_data.push((name, proj_type.to_string(), languages, main_tech, ports, docker.to_string(), deps_count));
663 }
664
665 let headers = vec!["Project", "Type", "Languages", "Main Tech", "Ports", "Docker", "Deps"];
667 let mut col_widths = headers.iter().map(|h| visual_width(h)).collect::<Vec<_>>();
668
669 for (name, proj_type, languages, main_tech, ports, docker, deps_count) in &project_data {
670 col_widths[0] = col_widths[0].max(visual_width(name));
671 col_widths[1] = col_widths[1].max(visual_width(proj_type));
672 col_widths[2] = col_widths[2].max(visual_width(languages));
673 col_widths[3] = col_widths[3].max(visual_width(main_tech));
674 col_widths[4] = col_widths[4].max(visual_width(ports));
675 col_widths[5] = col_widths[5].max(visual_width(docker));
676 col_widths[6] = col_widths[6].max(visual_width(deps_count));
677 }
678
679
680 let header_parts: Vec<String> = headers.iter().zip(&col_widths)
682 .map(|(h, &w)| format!("{:<width$}", h, width = w))
683 .collect();
684 let header_line = header_parts.join(" │ ");
685 box_drawer.add_value_only(&header_line);
686
687 let separator_parts: Vec<String> = col_widths.iter()
689 .map(|&w| "─".repeat(w))
690 .collect();
691 let separator_line = separator_parts.join("─┼─");
692 box_drawer.add_value_only(&separator_line);
693
694 for (name, proj_type, languages, main_tech, ports, docker, deps_count) in project_data {
696 let row_parts = vec![
697 format!("{:<width$}", name, width = col_widths[0]),
698 format!("{:<width$}", proj_type, width = col_widths[1]),
699 format!("{:<width$}", languages, width = col_widths[2]),
700 format!("{:<width$}", main_tech, width = col_widths[3]),
701 format!("{:<width$}", ports, width = col_widths[4]),
702 format!("{:<width$}", docker, width = col_widths[5]),
703 format!("{:<width$}", deps_count, width = col_widths[6]),
704 ];
705 let row_line = row_parts.join(" │ ");
706 box_drawer.add_value_only(&row_line);
707 }
708
709 println!("\n{}", box_drawer.draw());
710}
711
712fn display_projects_matrix_to_string(analysis: &MonorepoAnalysis) -> String {
714 let mut box_drawer = BoxDrawer::new("Projects Matrix");
715
716 for project in &analysis.projects {
718 let project_info = format!("{} ({})", project.name, format_project_category(&project.project_category));
719 box_drawer.add_value_only(&project_info);
720 }
721
722 format!("\n{}", box_drawer.draw())
723}
724
725fn display_single_project_matrix(analysis: &MonorepoAnalysis) {
727 if let Some(project) = analysis.projects.first() {
728 let mut box_drawer = BoxDrawer::new("Project Overview");
729
730 box_drawer.add_line("Name:", &project.name.yellow(), true);
732 box_drawer.add_line("Type:", &format_project_category(&project.project_category).green(), true);
733
734 if !project.analysis.languages.is_empty() {
736 let lang_info = project.analysis.languages.iter()
737 .map(|l| l.name.clone())
738 .collect::<Vec<_>>()
739 .join(", ");
740 box_drawer.add_line("Languages:", &lang_info.blue(), true);
741 }
742
743 if !project.analysis.technologies.is_empty() {
745 let tech_names = project.analysis.technologies.iter()
746 .take(3)
747 .map(|t| t.name.clone())
748 .collect::<Vec<_>>()
749 .join(", ");
750 box_drawer.add_line("Technologies:", &tech_names.magenta(), true);
751 }
752
753 box_drawer.add_separator();
755 box_drawer.add_line("Key Metrics:", "", true);
756
757 box_drawer.add_value_only(&format!("Entry Points: {} │ Exposed Ports: {} │ Env Variables: {}",
759 project.analysis.entry_points.len(),
760 project.analysis.ports.len(),
761 project.analysis.environment_variables.len()
762 ).cyan());
763
764 box_drawer.add_value_only(&format!("Build Scripts: {} │ Dependencies: {}",
765 project.analysis.build_scripts.len(),
766 project.analysis.dependencies.len()
767 ).cyan());
768
769 add_confidence_bar_to_drawer(project.analysis.analysis_metadata.confidence_score, &mut box_drawer);
771
772 println!("\n{}", box_drawer.draw());
773 }
774}
775
776fn display_single_project_matrix_to_string(analysis: &MonorepoAnalysis) -> String {
778 if let Some(project) = analysis.projects.first() {
779 let mut box_drawer = BoxDrawer::new("Project Overview");
780
781 box_drawer.add_line("Name:", &project.name.yellow(), true);
783 box_drawer.add_line("Type:", &format_project_category(&project.project_category).green(), true);
784
785 if !project.analysis.languages.is_empty() {
787 let lang_info = project.analysis.languages.iter()
788 .map(|l| l.name.clone())
789 .collect::<Vec<_>>()
790 .join(", ");
791 box_drawer.add_line("Languages:", &lang_info.blue(), true);
792 }
793
794 box_drawer.add_separator();
796 box_drawer.add_line("Key Metrics:", "", true);
797
798 box_drawer.add_value_only(&format!("Entry Points: {} │ Exposed Ports: {} │ Env Variables: {}",
799 project.analysis.entry_points.len(),
800 project.analysis.ports.len(),
801 project.analysis.environment_variables.len()
802 ).cyan());
803
804 box_drawer.add_value_only(&format!("Build Scripts: {} │ Dependencies: {}",
805 project.analysis.build_scripts.len(),
806 project.analysis.dependencies.len()
807 ).cyan());
808
809 format!("\n{}", box_drawer.draw())
810 } else {
811 String::new()
812 }
813}
814
815fn display_docker_overview_matrix(analysis: &MonorepoAnalysis) {
817 let mut box_drawer = BoxDrawer::new("Docker Infrastructure");
818
819 let mut total_dockerfiles = 0;
820 let mut total_compose_files = 0;
821 let mut total_services = 0;
822 let mut orchestration_patterns = std::collections::HashSet::new();
823
824 for project in &analysis.projects {
825 if let Some(docker) = &project.analysis.docker_analysis {
826 total_dockerfiles += docker.dockerfiles.len();
827 total_compose_files += docker.compose_files.len();
828 total_services += docker.services.len();
829 orchestration_patterns.insert(&docker.orchestration_pattern);
830 }
831 }
832
833 box_drawer.add_line("Dockerfiles:", &total_dockerfiles.to_string().yellow(), true);
834 box_drawer.add_line("Compose Files:", &total_compose_files.to_string().yellow(), true);
835 box_drawer.add_line("Total Services:", &total_services.to_string().yellow(), true);
836
837 let patterns = orchestration_patterns.iter()
838 .map(|p| format!("{:?}", p))
839 .collect::<Vec<_>>()
840 .join(", ");
841 box_drawer.add_line("Orchestration Patterns:", &patterns.green(), true);
842
843 let mut has_services = false;
845 for project in &analysis.projects {
846 if let Some(docker) = &project.analysis.docker_analysis {
847 for service in &docker.services {
848 if !service.ports.is_empty() || !service.depends_on.is_empty() {
849 has_services = true;
850 break;
851 }
852 }
853 }
854 }
855
856 if has_services {
857 box_drawer.add_separator();
858 box_drawer.add_line("Service Connectivity:", "", true);
859
860 for project in &analysis.projects {
861 if let Some(docker) = &project.analysis.docker_analysis {
862 for service in &docker.services {
863 if !service.ports.is_empty() || !service.depends_on.is_empty() {
864 let port_info = service.ports.iter()
865 .filter_map(|p| p.host_port.map(|hp| format!("{}:{}", hp, p.container_port)))
866 .collect::<Vec<_>>()
867 .join(", ");
868
869 let deps_info = if service.depends_on.is_empty() {
870 String::new()
871 } else {
872 format!(" → {}", service.depends_on.join(", "))
873 };
874
875 let info = format!(" {}: {}{}", service.name, port_info, deps_info);
876 box_drawer.add_value_only(&info.cyan());
877 }
878 }
879 }
880 }
881 }
882
883 println!("\n{}", box_drawer.draw());
884}
885
886fn display_docker_overview_matrix_to_string(analysis: &MonorepoAnalysis) -> String {
888 let mut box_drawer = BoxDrawer::new("Docker Infrastructure");
889
890 let mut total_dockerfiles = 0;
891 let mut total_compose_files = 0;
892 let mut total_services = 0;
893
894 for project in &analysis.projects {
895 if let Some(docker) = &project.analysis.docker_analysis {
896 total_dockerfiles += docker.dockerfiles.len();
897 total_compose_files += docker.compose_files.len();
898 total_services += docker.services.len();
899 }
900 }
901
902 box_drawer.add_line("Dockerfiles:", &total_dockerfiles.to_string().yellow(), true);
903 box_drawer.add_line("Compose Files:", &total_compose_files.to_string().yellow(), true);
904 box_drawer.add_line("Total Services:", &total_services.to_string().yellow(), true);
905
906 format!("\n{}", box_drawer.draw())
907}
908
909fn display_metrics_box(analysis: &MonorepoAnalysis) {
911 let mut box_drawer = BoxDrawer::new("Analysis Metrics");
912
913 let duration_ms = analysis.metadata.analysis_duration_ms;
915 let duration_str = if duration_ms < 1000 {
916 format!("{}ms", duration_ms)
917 } else {
918 format!("{:.1}s", duration_ms as f64 / 1000.0)
919 };
920
921 let metrics_line = format!(
923 "Duration: {} | Files: {} | Score: {}% | Version: {}",
924 duration_str,
925 analysis.metadata.files_analyzed,
926 format!("{:.0}", analysis.metadata.confidence_score * 100.0),
927 analysis.metadata.analyzer_version
928 );
929
930 let colored_metrics = metrics_line.cyan();
932 box_drawer.add_value_only(&colored_metrics.to_string());
933
934 println!("\n{}", box_drawer.draw());
935}
936
937fn display_metrics_box_to_string(analysis: &MonorepoAnalysis) -> String {
939 let mut box_drawer = BoxDrawer::new("Analysis Metrics");
940
941 let duration_ms = analysis.metadata.analysis_duration_ms;
943 let duration_str = if duration_ms < 1000 {
944 format!("{}ms", duration_ms)
945 } else {
946 format!("{:.1}s", duration_ms as f64 / 1000.0)
947 };
948
949 let metrics_line = format!(
951 "Duration: {} | Files: {} | Score: {}% | Version: {}",
952 duration_str,
953 analysis.metadata.files_analyzed,
954 format!("{:.0}", analysis.metadata.confidence_score * 100.0),
955 analysis.metadata.analyzer_version
956 );
957
958 box_drawer.add_value_only(&metrics_line.cyan());
959
960 format!("\n{}", box_drawer.draw())
961}
962
963fn add_confidence_bar_to_drawer(score: f32, box_drawer: &mut BoxDrawer) {
965 let percentage = (score * 100.0) as u8;
966 let bar_width = 20;
967 let filled = ((score * bar_width as f32) as usize).min(bar_width);
968
969 let bar = format!("{}{}",
970 "█".repeat(filled).green(),
971 "░".repeat(bar_width - filled).dimmed()
972 );
973
974 let color = if percentage >= 80 {
975 "green"
976 } else if percentage >= 60 {
977 "yellow"
978 } else {
979 "red"
980 };
981
982 let confidence_info = format!("{} {}", bar, format!("{:.0}%", percentage).color(color));
983 box_drawer.add_line("Confidence:", &confidence_info, true);
984}
985
986fn get_main_technologies(technologies: &[DetectedTechnology]) -> String {
988 let primary = technologies.iter().find(|t| t.is_primary);
989 let frameworks: Vec<_> = technologies.iter()
990 .filter(|t| matches!(t.category, TechnologyCategory::FrontendFramework | TechnologyCategory::MetaFramework))
991 .take(2)
992 .collect();
993
994 let mut result = Vec::new();
995
996 if let Some(p) = primary {
997 result.push(p.name.clone());
998 }
999
1000 for f in frameworks {
1001 if Some(&f.name) != primary.map(|p| &p.name) {
1002 result.push(f.name.clone());
1003 }
1004 }
1005
1006 if result.is_empty() {
1007 "-".to_string()
1008 } else {
1009 result.join(", ")
1010 }
1011}
1012
1013pub fn display_detailed_view(analysis: &MonorepoAnalysis) {
1015 println!("{}", "=".repeat(80));
1017 println!("\n📊 PROJECT ANALYSIS RESULTS");
1018 println!("{}", "=".repeat(80));
1019
1020 if analysis.is_monorepo {
1022 println!("\n🏗️ Architecture: Monorepo with {} projects", analysis.projects.len());
1023 println!(" Pattern: {:?}", analysis.technology_summary.architecture_pattern);
1024
1025 display_architecture_description(&analysis.technology_summary.architecture_pattern);
1026 } else {
1027 println!("\n🏗️ Architecture: Single Project");
1028 }
1029
1030 println!("\n🌐 Technology Summary:");
1032 if !analysis.technology_summary.languages.is_empty() {
1033 println!(" Languages: {}", analysis.technology_summary.languages.join(", "));
1034 }
1035 if !analysis.technology_summary.frameworks.is_empty() {
1036 println!(" Frameworks: {}", analysis.technology_summary.frameworks.join(", "));
1037 }
1038 if !analysis.technology_summary.databases.is_empty() {
1039 println!(" Databases: {}", analysis.technology_summary.databases.join(", "));
1040 }
1041
1042 println!("\n📁 Project Details:");
1044 println!("{}", "=".repeat(80));
1045
1046 for (i, project) in analysis.projects.iter().enumerate() {
1047 println!("\n{} {}. {} ({})",
1048 get_category_emoji(&project.project_category),
1049 i + 1,
1050 project.name,
1051 format_project_category(&project.project_category)
1052 );
1053
1054 if analysis.is_monorepo {
1055 println!(" 📂 Path: {}", project.path.display());
1056 }
1057
1058 if !project.analysis.languages.is_empty() {
1060 println!(" 🌐 Languages:");
1061 for lang in &project.analysis.languages {
1062 print!(" • {} (confidence: {:.1}%)", lang.name, lang.confidence * 100.0);
1063 if let Some(version) = &lang.version {
1064 print!(" - Version: {}", version);
1065 }
1066 println!();
1067 }
1068 }
1069
1070 if !project.analysis.technologies.is_empty() {
1072 println!(" 🚀 Technologies:");
1073 display_technologies_detailed_legacy(&project.analysis.technologies);
1074 }
1075
1076 if !project.analysis.entry_points.is_empty() {
1078 println!(" 📍 Entry Points ({}):", project.analysis.entry_points.len());
1079 for (j, entry) in project.analysis.entry_points.iter().enumerate() {
1080 println!(" {}. File: {}", j + 1, entry.file.display());
1081 if let Some(func) = &entry.function {
1082 println!(" Function: {}", func);
1083 }
1084 if let Some(cmd) = &entry.command {
1085 println!(" Command: {}", cmd);
1086 }
1087 }
1088 }
1089
1090 if !project.analysis.ports.is_empty() {
1092 println!(" 🔌 Exposed Ports ({}):", project.analysis.ports.len());
1093 for port in &project.analysis.ports {
1094 println!(" • Port {}: {:?}", port.number, port.protocol);
1095 if let Some(desc) = &port.description {
1096 println!(" {}", desc);
1097 }
1098 }
1099 }
1100
1101 if !project.analysis.environment_variables.is_empty() {
1103 println!(" 🔐 Environment Variables ({}):", project.analysis.environment_variables.len());
1104 let required_vars: Vec<_> = project.analysis.environment_variables.iter()
1105 .filter(|ev| ev.required)
1106 .collect();
1107 let optional_vars: Vec<_> = project.analysis.environment_variables.iter()
1108 .filter(|ev| !ev.required)
1109 .collect();
1110
1111 if !required_vars.is_empty() {
1112 println!(" Required:");
1113 for var in required_vars {
1114 println!(" • {} {}",
1115 var.name,
1116 if let Some(desc) = &var.description {
1117 format!("({})", desc)
1118 } else {
1119 String::new()
1120 }
1121 );
1122 }
1123 }
1124
1125 if !optional_vars.is_empty() {
1126 println!(" Optional:");
1127 for var in optional_vars {
1128 println!(" • {} = {:?}",
1129 var.name,
1130 var.default_value.as_deref().unwrap_or("no default")
1131 );
1132 }
1133 }
1134 }
1135
1136 if !project.analysis.build_scripts.is_empty() {
1138 println!(" 🔨 Build Scripts ({}):", project.analysis.build_scripts.len());
1139 let default_scripts: Vec<_> = project.analysis.build_scripts.iter()
1140 .filter(|bs| bs.is_default)
1141 .collect();
1142 let other_scripts: Vec<_> = project.analysis.build_scripts.iter()
1143 .filter(|bs| !bs.is_default)
1144 .collect();
1145
1146 if !default_scripts.is_empty() {
1147 println!(" Default scripts:");
1148 for script in default_scripts {
1149 println!(" • {}: {}", script.name, script.command);
1150 if let Some(desc) = &script.description {
1151 println!(" {}", desc);
1152 }
1153 }
1154 }
1155
1156 if !other_scripts.is_empty() {
1157 println!(" Other scripts:");
1158 for script in other_scripts {
1159 println!(" • {}: {}", script.name, script.command);
1160 if let Some(desc) = &script.description {
1161 println!(" {}", desc);
1162 }
1163 }
1164 }
1165 }
1166
1167 if !project.analysis.dependencies.is_empty() {
1169 println!(" 📦 Dependencies ({}):", project.analysis.dependencies.len());
1170 if project.analysis.dependencies.len() <= 5 {
1171 for (name, version) in &project.analysis.dependencies {
1172 println!(" • {} v{}", name, version);
1173 }
1174 } else {
1175 for (name, version) in project.analysis.dependencies.iter().take(5) {
1177 println!(" • {} v{}", name, version);
1178 }
1179 println!(" ... and {} more", project.analysis.dependencies.len() - 5);
1180 }
1181 }
1182
1183 if let Some(docker_analysis) = &project.analysis.docker_analysis {
1185 display_docker_analysis_detailed_legacy(docker_analysis);
1186 }
1187
1188 println!(" 🎯 Project Type: {:?}", project.analysis.project_type);
1190
1191 if i < analysis.projects.len() - 1 {
1192 println!("{}", "-".repeat(40));
1193 }
1194 }
1195
1196 println!("\n📋 ANALYSIS SUMMARY");
1198 println!("{}", "=".repeat(80));
1199 println!("✅ Project Analysis Complete!");
1200
1201 if analysis.is_monorepo {
1202 println!("\n🏗️ Monorepo Architecture:");
1203 println!(" • Total projects: {}", analysis.projects.len());
1204 println!(" • Architecture pattern: {:?}", analysis.technology_summary.architecture_pattern);
1205
1206 let frontend_count = analysis.projects.iter().filter(|p| p.project_category == ProjectCategory::Frontend).count();
1207 let backend_count = analysis.projects.iter().filter(|p| matches!(p.project_category, ProjectCategory::Backend | ProjectCategory::Api)).count();
1208 let service_count = analysis.projects.iter().filter(|p| p.project_category == ProjectCategory::Service).count();
1209 let lib_count = analysis.projects.iter().filter(|p| p.project_category == ProjectCategory::Library).count();
1210
1211 if frontend_count > 0 { println!(" • Frontend projects: {}", frontend_count); }
1212 if backend_count > 0 { println!(" • Backend/API projects: {}", backend_count); }
1213 if service_count > 0 { println!(" • Service projects: {}", service_count); }
1214 if lib_count > 0 { println!(" • Library projects: {}", lib_count); }
1215 }
1216
1217 println!("\n📈 Analysis Metadata:");
1218 println!(" • Duration: {}ms", analysis.metadata.analysis_duration_ms);
1219 println!(" • Files analyzed: {}", analysis.metadata.files_analyzed);
1220 println!(" • Confidence score: {:.1}%", analysis.metadata.confidence_score * 100.0);
1221 println!(" • Analyzer version: {}", analysis.metadata.analyzer_version);
1222}
1223
1224fn display_technologies_detailed_legacy(technologies: &[DetectedTechnology]) {
1226 let mut by_category: std::collections::HashMap<&TechnologyCategory, Vec<&DetectedTechnology>> = std::collections::HashMap::new();
1228
1229 for tech in technologies {
1230 by_category.entry(&tech.category).or_insert_with(Vec::new).push(tech);
1231 }
1232
1233 if let Some(primary) = technologies.iter().find(|t| t.is_primary) {
1235 println!("\n🛠️ Technology Stack:");
1236 println!(" 🎯 PRIMARY: {} (confidence: {:.1}%)", primary.name, primary.confidence * 100.0);
1237 println!(" Architecture driver for this project");
1238 }
1239
1240 let categories = [
1242 (TechnologyCategory::MetaFramework, "🏗️ Meta-Frameworks"),
1243 (TechnologyCategory::BackendFramework, "🖥️ Backend Frameworks"),
1244 (TechnologyCategory::FrontendFramework, "🎨 Frontend Frameworks"),
1245 (TechnologyCategory::Library(LibraryType::UI), "🎨 UI Libraries"),
1246 (TechnologyCategory::Library(LibraryType::Utility), "📚 Core Libraries"),
1247 (TechnologyCategory::BuildTool, "🔨 Build Tools"),
1248 (TechnologyCategory::PackageManager, "📦 Package Managers"),
1249 (TechnologyCategory::Database, "🗃️ Database & ORM"),
1250 (TechnologyCategory::Runtime, "⚡ Runtimes"),
1251 (TechnologyCategory::Testing, "🧪 Testing"),
1252 ];
1253
1254 for (category, label) in &categories {
1255 if let Some(techs) = by_category.get(category) {
1256 if !techs.is_empty() {
1257 println!("\n {}:", label);
1258 for tech in techs {
1259 println!(" • {} (confidence: {:.1}%)", tech.name, tech.confidence * 100.0);
1260 if let Some(version) = &tech.version {
1261 println!(" Version: {}", version);
1262 }
1263 }
1264 }
1265 }
1266 }
1267
1268 for (cat, techs) in &by_category {
1270 match cat {
1271 TechnologyCategory::Library(lib_type) => {
1272 let label = match lib_type {
1273 LibraryType::StateManagement => "🔄 State Management",
1274 LibraryType::DataFetching => "🔃 Data Fetching",
1275 LibraryType::Routing => "🗺️ Routing",
1276 LibraryType::Styling => "🎨 Styling",
1277 LibraryType::HttpClient => "🌐 HTTP Clients",
1278 LibraryType::Authentication => "🔐 Authentication",
1279 LibraryType::Other(_) => "📦 Other Libraries",
1280 _ => continue, };
1282
1283 if !matches!(lib_type, LibraryType::UI | LibraryType::Utility) && !techs.is_empty() {
1285 println!("\n {}:", label);
1286 for tech in techs {
1287 println!(" • {} (confidence: {:.1}%)", tech.name, tech.confidence * 100.0);
1288 if let Some(version) = &tech.version {
1289 println!(" Version: {}", version);
1290 }
1291 }
1292 }
1293 }
1294 _ => {} }
1296 }
1297}
1298
1299fn display_docker_analysis_detailed_legacy(docker_analysis: &DockerAnalysis) {
1301 println!("\n 🐳 Docker Infrastructure Analysis:");
1302
1303 if !docker_analysis.dockerfiles.is_empty() {
1305 println!(" 📄 Dockerfiles ({}):", docker_analysis.dockerfiles.len());
1306 for dockerfile in &docker_analysis.dockerfiles {
1307 println!(" • {}", dockerfile.path.display());
1308 if let Some(env) = &dockerfile.environment {
1309 println!(" Environment: {}", env);
1310 }
1311 if let Some(base_image) = &dockerfile.base_image {
1312 println!(" Base image: {}", base_image);
1313 }
1314 if !dockerfile.exposed_ports.is_empty() {
1315 println!(" Exposed ports: {}",
1316 dockerfile.exposed_ports.iter().map(|p| p.to_string()).collect::<Vec<_>>().join(", "));
1317 }
1318 if dockerfile.is_multistage {
1319 println!(" Multi-stage build: {} stages", dockerfile.build_stages.len());
1320 }
1321 println!(" Instructions: {}", dockerfile.instruction_count);
1322 }
1323 }
1324
1325 if !docker_analysis.compose_files.is_empty() {
1327 println!(" 📋 Compose Files ({}):", docker_analysis.compose_files.len());
1328 for compose_file in &docker_analysis.compose_files {
1329 println!(" • {}", compose_file.path.display());
1330 if let Some(env) = &compose_file.environment {
1331 println!(" Environment: {}", env);
1332 }
1333 if let Some(version) = &compose_file.version {
1334 println!(" Version: {}", version);
1335 }
1336 if !compose_file.service_names.is_empty() {
1337 println!(" Services: {}", compose_file.service_names.join(", "));
1338 }
1339 if !compose_file.networks.is_empty() {
1340 println!(" Networks: {}", compose_file.networks.join(", "));
1341 }
1342 if !compose_file.volumes.is_empty() {
1343 println!(" Volumes: {}", compose_file.volumes.join(", "));
1344 }
1345 }
1346 }
1347
1348 println!(" 🏗️ Orchestration Pattern: {:?}", docker_analysis.orchestration_pattern);
1350 match docker_analysis.orchestration_pattern {
1351 OrchestrationPattern::SingleContainer => {
1352 println!(" Simple containerized application");
1353 }
1354 OrchestrationPattern::DockerCompose => {
1355 println!(" Multi-service Docker Compose setup");
1356 }
1357 OrchestrationPattern::Microservices => {
1358 println!(" Microservices architecture with service discovery");
1359 }
1360 OrchestrationPattern::EventDriven => {
1361 println!(" Event-driven architecture with message queues");
1362 }
1363 OrchestrationPattern::ServiceMesh => {
1364 println!(" Service mesh for advanced service communication");
1365 }
1366 OrchestrationPattern::Mixed => {
1367 println!(" Mixed/complex orchestration pattern");
1368 }
1369 }
1370}
1371
1372fn display_architecture_description(pattern: &ArchitecturePattern) {
1374 match pattern {
1375 ArchitecturePattern::Monolithic => {
1376 println!(" 📦 This is a single, self-contained application");
1377 }
1378 ArchitecturePattern::Fullstack => {
1379 println!(" 🌐 This is a full-stack application with separate frontend and backend");
1380 }
1381 ArchitecturePattern::Microservices => {
1382 println!(" 🔗 This is a microservices architecture with multiple independent services");
1383 }
1384 ArchitecturePattern::ApiFirst => {
1385 println!(" 🔌 This is an API-first architecture focused on service interfaces");
1386 }
1387 ArchitecturePattern::EventDriven => {
1388 println!(" 📡 This is an event-driven architecture with decoupled components");
1389 }
1390 ArchitecturePattern::Mixed => {
1391 println!(" 🔀 This is a mixed architecture combining multiple patterns");
1392 }
1393 }
1394}
1395
1396pub fn display_summary_view(analysis: &MonorepoAnalysis) {
1398 println!("\n{} {}", "▶".bright_blue(), "PROJECT ANALYSIS SUMMARY".bright_white().bold());
1399 println!("{}", "─".repeat(50).dimmed());
1400
1401 println!("{} Architecture: {}", "│".dimmed(),
1402 if analysis.is_monorepo {
1403 format!("Monorepo ({} projects)", analysis.projects.len()).yellow()
1404 } else {
1405 "Single Project".to_string().yellow()
1406 }
1407 );
1408
1409 println!("{} Pattern: {}", "│".dimmed(), format!("{:?}", analysis.technology_summary.architecture_pattern).green());
1410 println!("{} Stack: {}", "│".dimmed(), analysis.technology_summary.languages.join(", ").blue());
1411
1412 if !analysis.technology_summary.frameworks.is_empty() {
1413 println!("{} Frameworks: {}", "│".dimmed(), analysis.technology_summary.frameworks.join(", ").magenta());
1414 }
1415
1416 println!("{} Analysis Time: {}ms", "│".dimmed(), analysis.metadata.analysis_duration_ms);
1417 println!("{} Confidence: {:.0}%", "│".dimmed(), analysis.metadata.confidence_score * 100.0);
1418
1419 println!("{}", "─".repeat(50).dimmed());
1420}
1421
1422pub fn display_json_view(analysis: &MonorepoAnalysis) {
1424 match serde_json::to_string_pretty(analysis) {
1425 Ok(json) => println!("{}", json),
1426 Err(e) => eprintln!("Error serializing to JSON: {}", e),
1427 }
1428}
1429
1430pub fn display_json_view_to_string(analysis: &MonorepoAnalysis) -> String {
1432 match serde_json::to_string_pretty(analysis) {
1433 Ok(json) => json,
1434 Err(e) => format!("Error serializing to JSON: {}", e),
1435 }
1436}
1437
1438pub fn display_summary_view_to_string(analysis: &MonorepoAnalysis) -> String {
1440 let mut output = String::new();
1441
1442 output.push_str(&format!("\n{} {}\n", "▶".bright_blue(), "PROJECT ANALYSIS SUMMARY".bright_white().bold()));
1443 output.push_str(&format!("{}\n", "─".repeat(50).dimmed()));
1444
1445 output.push_str(&format!("{} Architecture: {}\n", "│".dimmed(),
1446 if analysis.is_monorepo {
1447 format!("Monorepo ({} projects)", analysis.projects.len()).yellow()
1448 } else {
1449 "Single Project".to_string().yellow()
1450 }
1451 ));
1452
1453 output.push_str(&format!("{} Pattern: {}\n", "│".dimmed(), format!("{:?}", analysis.technology_summary.architecture_pattern).green()));
1454 output.push_str(&format!("{} Stack: {}\n", "│".dimmed(), analysis.technology_summary.languages.join(", ").blue()));
1455
1456 if !analysis.technology_summary.frameworks.is_empty() {
1457 output.push_str(&format!("{} Frameworks: {}\n", "│".dimmed(), analysis.technology_summary.frameworks.join(", ").magenta()));
1458 }
1459
1460 output.push_str(&format!("{} Analysis Time: {}ms\n", "│".dimmed(), analysis.metadata.analysis_duration_ms));
1461 output.push_str(&format!("{} Confidence: {:.0}%\n", "│".dimmed(), analysis.metadata.confidence_score * 100.0));
1462
1463 output.push_str(&format!("{}\n", "─".repeat(50).dimmed()));
1464
1465 output
1466}
1467
1468pub fn display_detailed_view_to_string(analysis: &MonorepoAnalysis) -> String {
1470 let mut output = String::new();
1471
1472 output.push_str(&format!("{}\n", "=".repeat(80)));
1473 output.push_str("\n📊 PROJECT ANALYSIS RESULTS\n");
1474 output.push_str(&format!("{}\n", "=".repeat(80)));
1475
1476 if analysis.is_monorepo {
1478 output.push_str(&format!("\n🏗️ Architecture: Monorepo with {} projects\n", analysis.projects.len()));
1479 output.push_str(&format!(" Pattern: {:?}\n", analysis.technology_summary.architecture_pattern));
1480
1481 output.push_str(&display_architecture_description_to_string(&analysis.technology_summary.architecture_pattern));
1482 } else {
1483 output.push_str("\n🏗️ Architecture: Single Project\n");
1484 }
1485
1486 output.push_str("\n🌐 Technology Summary:\n");
1488 if !analysis.technology_summary.languages.is_empty() {
1489 output.push_str(&format!(" Languages: {}\n", analysis.technology_summary.languages.join(", ")));
1490 }
1491 if !analysis.technology_summary.frameworks.is_empty() {
1492 output.push_str(&format!(" Frameworks: {}\n", analysis.technology_summary.frameworks.join(", ")));
1493 }
1494 if !analysis.technology_summary.databases.is_empty() {
1495 output.push_str(&format!(" Databases: {}\n", analysis.technology_summary.databases.join(", ")));
1496 }
1497
1498 output.push_str("\n📁 Project Details:\n");
1500 output.push_str(&format!("{}\n", "=".repeat(80)));
1501
1502 for (i, project) in analysis.projects.iter().enumerate() {
1503 output.push_str(&format!("\n{} {}. {} ({})\n",
1504 get_category_emoji(&project.project_category),
1505 i + 1,
1506 project.name,
1507 format_project_category(&project.project_category)
1508 ));
1509
1510 if analysis.is_monorepo {
1511 output.push_str(&format!(" 📂 Path: {}\n", project.path.display()));
1512 }
1513
1514 if !project.analysis.languages.is_empty() {
1516 output.push_str(" 🌐 Languages:\n");
1517 for lang in &project.analysis.languages {
1518 output.push_str(&format!(" • {} (confidence: {:.1}%)", lang.name, lang.confidence * 100.0));
1519 if let Some(version) = &lang.version {
1520 output.push_str(&format!(" - Version: {}", version));
1521 }
1522 output.push('\n');
1523 }
1524 }
1525
1526 if i < analysis.projects.len() - 1 {
1527 output.push_str(&format!("{}\n", "-".repeat(40)));
1528 }
1529 }
1530
1531 output.push_str("\n📋 ANALYSIS SUMMARY\n");
1533 output.push_str(&format!("{}\n", "=".repeat(80)));
1534 output.push_str("✅ Project Analysis Complete!\n");
1535
1536 output.push_str("\n📈 Analysis Metadata:\n");
1537 output.push_str(&format!(" • Duration: {}ms\n", analysis.metadata.analysis_duration_ms));
1538 output.push_str(&format!(" • Files analyzed: {}\n", analysis.metadata.files_analyzed));
1539 output.push_str(&format!(" • Confidence score: {:.1}%\n", analysis.metadata.confidence_score * 100.0));
1540 output.push_str(&format!(" • Analyzer version: {}\n", analysis.metadata.analyzer_version));
1541
1542 output
1543}
1544
1545fn display_architecture_description_to_string(pattern: &ArchitecturePattern) -> String {
1547 match pattern {
1548 ArchitecturePattern::Monolithic => {
1549 " 📦 This is a single, self-contained application\n".to_string()
1550 }
1551 ArchitecturePattern::Fullstack => {
1552 " 🌐 This is a full-stack application with separate frontend and backend\n".to_string()
1553 }
1554 ArchitecturePattern::Microservices => {
1555 " 🔗 This is a microservices architecture with multiple independent services\n".to_string()
1556 }
1557 ArchitecturePattern::ApiFirst => {
1558 " 🔌 This is an API-first architecture focused on service interfaces\n".to_string()
1559 }
1560 ArchitecturePattern::EventDriven => {
1561 " 📡 This is an event-driven architecture with decoupled components\n".to_string()
1562 }
1563 ArchitecturePattern::Mixed => {
1564 " 🔀 This is a mixed architecture combining multiple patterns\n".to_string()
1565 }
1566 }
1567}
1568
1569fn get_category_emoji(category: &ProjectCategory) -> &'static str {
1571 match category {
1572 ProjectCategory::Frontend => "🌐",
1573 ProjectCategory::Backend => "⚙️",
1574 ProjectCategory::Api => "🔌",
1575 ProjectCategory::Service => "🚀",
1576 ProjectCategory::Library => "📚",
1577 ProjectCategory::Tool => "🔧",
1578 ProjectCategory::Documentation => "📖",
1579 ProjectCategory::Infrastructure => "🏗️",
1580 ProjectCategory::Unknown => "❓",
1581 }
1582}
1583
1584fn format_project_category(category: &ProjectCategory) -> &'static str {
1586 match category {
1587 ProjectCategory::Frontend => "Frontend",
1588 ProjectCategory::Backend => "Backend",
1589 ProjectCategory::Api => "API",
1590 ProjectCategory::Service => "Service",
1591 ProjectCategory::Library => "Library",
1592 ProjectCategory::Tool => "Tool",
1593 ProjectCategory::Documentation => "Documentation",
1594 ProjectCategory::Infrastructure => "Infrastructure",
1595 ProjectCategory::Unknown => "Unknown",
1596 }
1597}
1598
1599fn add_technologies_to_drawer(technologies: &[DetectedTechnology], box_drawer: &mut BoxDrawer) {
1601 let mut by_category: std::collections::HashMap<&TechnologyCategory, Vec<&DetectedTechnology>> = std::collections::HashMap::new();
1602
1603 for tech in technologies {
1604 by_category.entry(&tech.category).or_insert_with(Vec::new).push(tech);
1605 }
1606
1607 if let Some(primary) = technologies.iter().find(|t| t.is_primary) {
1609 let primary_info = primary.name.bright_yellow().bold().to_string();
1610 box_drawer.add_line("Primary Stack:", &primary_info, true);
1611 }
1612
1613 let categories = [
1615 (TechnologyCategory::FrontendFramework, "Frameworks"),
1616 (TechnologyCategory::BuildTool, "Build Tools"),
1617 (TechnologyCategory::Database, "Databases"),
1618 (TechnologyCategory::Testing, "Testing"),
1619 ];
1620
1621 for (category, label) in &categories {
1622 if let Some(techs) = by_category.get(category) {
1623 let tech_names = techs.iter()
1624 .map(|t| t.name.clone())
1625 .collect::<Vec<_>>()
1626 .join(", ");
1627
1628 if !tech_names.is_empty() {
1629 let label_with_colon = format!("{}:", label);
1630 box_drawer.add_line(&label_with_colon, &tech_names.magenta(), true);
1631 }
1632 }
1633 }
1634
1635 let mut all_libraries: Vec<&DetectedTechnology> = Vec::new();
1637 for (cat, techs) in &by_category {
1638 if matches!(cat, TechnologyCategory::Library(_)) {
1639 all_libraries.extend(techs.iter().copied());
1640 }
1641 }
1642
1643 if !all_libraries.is_empty() {
1644 all_libraries.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal));
1646
1647 if all_libraries.len() <= 3 {
1648 let tech_names = all_libraries.iter()
1650 .map(|t| t.name.clone())
1651 .collect::<Vec<_>>()
1652 .join(", ");
1653 box_drawer.add_line("Libraries:", &tech_names.magenta(), true);
1654 } else {
1655 box_drawer.add_line("Libraries:", "", true);
1657
1658 let items_per_row = 3;
1660 for chunk in all_libraries.chunks(items_per_row) {
1661 let row_items = chunk.iter()
1662 .map(|t| t.name.clone())
1663 .collect::<Vec<_>>()
1664 .join(", ");
1665
1666 let indented_row = format!(" {}", row_items);
1668 box_drawer.add_value_only(&indented_row.magenta());
1669 }
1670 }
1671 }
1672}
1673
1674#[cfg(test)]
1675mod tests {
1676 use super::*;
1677
1678 #[test]
1679 fn test_display_modes() {
1680 assert_eq!(DisplayMode::Matrix, DisplayMode::Matrix);
1682 assert_ne!(DisplayMode::Matrix, DisplayMode::Detailed);
1683 }
1684}