diff --git a/src/depcheck/reporters/terminal.py b/src/depcheck/reporters/terminal.py new file mode 100644 index 0000000..54e0c8e --- /dev/null +++ b/src/depcheck/reporters/terminal.py @@ -0,0 +1,129 @@ +"""Terminal reporter with Rich formatting.""" + +from typing import Optional + +from rich import box +from rich.console import Console +from rich.panel import Panel +from rich.table import Table +from rich.text import Text + +from depcheck.config import Config +from depcheck.models import ScanResult, Severity + + +class TerminalReporter: + """Report results in terminal-friendly format using Rich.""" + + def __init__(self, config: Optional[Config] = None): + self.config = config or Config() + self.console = Console() + + def report(self, result: ScanResult) -> None: + """Display the scan result.""" + self._print_summary(result) + self._print_vulnerabilities(result) + self._print_outdated_dependencies(result) + + def _print_summary(self, result: ScanResult) -> None: + """Print scan summary.""" + status = "✓ All Fresh" if result.outdated_count == 0 and result.vulnerable_count == 0 else "⚠ Issues Found" + status_color = "green" if result.outdated_count == 0 and result.vulnerable_count == 0 else "yellow" + + summary = Table(box=box.SIMPLE, show_header=False, padding=0) + summary.add_column() + summary.add_column() + + summary.add_row("Status", Text(status, style=status_color)) + summary.add_row("Dependencies Scanned", str(len(result.dependencies))) + summary.add_row("Outdated Packages", str(result.outdated_count)) + summary.add_row("Vulnerabilities", str(result.vulnerable_count)) + if result.source_file: + summary.add_row("Source File", result.source_file) + + self.console.print(Panel(summary, title="Dependency Scan Results", expand=False)) + + def _print_vulnerabilities(self, result: ScanResult) -> None: + """Print detected vulnerabilities.""" + if not result.vulnerabilities: + return + + vuln_table = Table(title="Security Vulnerabilities Detected", box=box.HEAVY_EDGE) + vuln_table.add_column("Package", style="cyan") + vuln_table.add_column("Current", style="red") + vuln_table.add_column("CVE ID", style="yellow") + vuln_table.add_column("Severity", style="bold") + vuln_table.add_column("Description") + + severity_colors = { + Severity.CRITICAL: "red bold", + Severity.HIGH: "orange1", + Severity.MEDIUM: "yellow", + Severity.LOW: "blue", + Severity.INFO: "white", + } + + for dep, vuln in result.vulnerabilities: + severity_style = severity_colors.get(vuln.severity, "white") + vuln_table.add_row( + dep.name, + dep.current_version, + vuln.cve_id, + Text(vuln.severity.value.upper(), style=severity_style), + vuln.description, + ) + + self.console.print(Panel(vuln_table, title="⚠ Security Vulnerabilities", expand=False)) + + def _print_outdated_dependencies(self, result: ScanResult) -> None: + """Print outdated dependencies.""" + outdated = [d for d in result.dependencies if d.is_outdated] + if not outdated: + return + + outdated_table = Table(title="Outdated Packages", box=box.HEAVY_EDGE) + outdated_table.add_column("Package", style="cyan") + outdated_table.add_column("Current", style="yellow") + outdated_table.add_column("Latest", style="green") + outdated_table.add_column("Update Type", style="magenta") + outdated_table.add_column("Category", style="white") + + for dep in outdated: + update_type = "" + if dep.latest_version: + parts = dep.current_version.split(".") + latest_parts = dep.latest_version.split(".") + if latest_parts[0] > parts[0]: + update_type = "major" + elif len(latest_parts) > 1 and len(parts) > 1 and latest_parts[1] > parts[1]: + update_type = "minor" + elif len(latest_parts) > 2 and len(parts) > 2 and latest_parts[2] > parts[2]: + update_type = "patch" + + outdated_table.add_row( + dep.name, + dep.current_version, + dep.latest_version or "unknown", + update_type or "-", + dep.category, + ) + + self.console.print(Panel(outdated_table, title="Outdated Packages", expand=False)) + + def print_error(self, message: str) -> None: + """Print error message.""" + self.console.print(Panel(Text(message, style="red"), title="Error", expand=False)) + + def print_warning(self, message: str) -> None: + """Print warning message.""" + self.console.print(Panel(Text(message, style="yellow"), title="Warning", expand=False)) + + def print_info(self, message: str) -> None: + """Print info message.""" + self.console.print(Text(message, style="blue")) + + def get_exit_code(self, result: ScanResult) -> int: + """Determine exit code based on results.""" + from depcheck.reporters.json import JSONReporter + + return JSONReporter(self.config).get_exit_code(result)