/* ©AngelaMos | 2026 output.go Terminal output functions for scan results, updates, skips, and errors Formats and prints all user-facing output: the update table with per-change type colors, the vulnerability report in compact or verbose mode, the skipped-packages list, the summary block, and error messages. Reads the package-level verbose and minSeverity flags defined in update.go. Key exports: PrintUpdates - renders the table of available dependency updates PrintSkipped - lists packages skipped during update resolution PrintVulnerabilities - renders vulnerability findings in compact or verbose mode PrintSummary - prints final scan totals and elapsed time PrintError - prints a formatted error message to stdout Connects to: update.go - calls all Print functions after scan completes pypi - uses ChangeKind (Major, Minor) for update color selection ui - uses color functions and symbol constants for formatting types.go - accepts UpdateResult, Vulnerability, ScanResult */ package cli import ( "fmt" "sort" "strings" "github.com/CarterPerez-dev/angela/internal/pypi" "github.com/CarterPerez-dev/angela/internal/ui" "github.com/CarterPerez-dev/angela/pkg/types" ) const ( severityCritical = "CRITICAL" severityHigh = "HIGH" severityModerate = "MODERATE" severityLow = "LOW" ) func printDivider() { fmt.Printf("\n %s\n", ui.HiBlack(ui.HRule(44))) } func printSectionHeader( symbol, title string, symbolColor, titleColor func(a ...any) string, ) { fmt.Printf( "\n %s %s\n", symbolColor(symbol), titleColor(title), ) } func PrintUpdates(updates []types.UpdateResult) { var actionable []types.UpdateResult for _, u := range updates { if !u.Skipped { actionable = append(actionable, u) } } if len(actionable) == 0 { printDivider() fmt.Printf( "\n %s %s\n", ui.HiGreen(ui.Check), ui.HiBlackItalic( "All dependencies are up to date.", ), ) return } printDivider() printSectionHeader( ui.Diamond, "Updates available", ui.Cyan, ui.Red, ) nameWidth := 0 oldWidth := 0 for _, u := range actionable { if len(u.Name) > nameWidth { nameWidth = len(u.Name) } if len(u.OldVer) > oldWidth { oldWidth = len(u.OldVer) } } for _, u := range actionable { changeColor := changeColorFn(u.Change) fmt.Printf( " %-*s %s %s %s %s %s\n", nameWidth, ui.HiBlue(u.Name), ui.HiBlackCrossed( padRight(u.OldVer, oldWidth), ), ui.Green(ui.Arrow), changeColor(u.NewVer), ui.HiBlack(ui.ArrowRight), changeColor(u.Change), ) } } func PrintSkipped(updates []types.UpdateResult) { var skipped []types.UpdateResult for _, u := range updates { if u.Skipped { skipped = append(skipped, u) } } if len(skipped) == 0 { return } printDivider() printSectionHeader( ui.ArrowRight, "Skipped", ui.Yellow, ui.Red, ) for _, u := range skipped { fmt.Printf( " %s %s\n", ui.HiBlue(u.Name), ui.HiBlackItalic(u.Reason), ) } } func PrintVulnerabilities( vulns map[string][]types.Vulnerability, ) { if len(vulns) == 0 { return } total := 0 for _, vl := range vulns { total += len(vl) } printDivider() fmt.Printf( "\n %s %s %s %s %s %s\n", ui.Red(ui.TriangleUp), ui.Red("Vulnerabilities:"), ui.RedBold(fmt.Sprintf("%d", total)), ui.HiBlack("across"), ui.RedBold(fmt.Sprintf("%d", len(vulns))), ui.HiBlack(pluralize("package", len(vulns))), ) pkgs := sortedVulnPackages(vulns) if verbose { printVulnsVerbose(pkgs, vulns) } else { printVulnsCompact(pkgs, vulns) } } const maxVulnsPerPackage = 5 func printVulnsCompact( pkgs []string, vulns map[string][]types.Vulnerability, ) { for _, pkg := range pkgs { vlist := vulns[pkg] sortVulnsBySeverity(vlist) fmt.Printf( "\n %s %s %s\n", ui.Blue(ui.Gem), ui.HiCyan(pkg), ui.HiBlack(fmt.Sprintf( "(%d %s: %s)", len(vlist), pluralize("vuln", len(vlist)), severityBreakdown(vlist), )), ) limit := min(maxVulnsPerPackage, len(vlist)) for i := range limit { printVulnLine(vlist[i]) } if len(vlist) > maxVulnsPerPackage { fmt.Printf( " %s\n", ui.HiBlackItalic(fmt.Sprintf( "...and %d more", len(vlist)-maxVulnsPerPackage, )), ) } } fmt.Printf( "\n %s\n", ui.HiBlackItalic( "Run angela scan -v for full details.", ), ) } func printVulnsVerbose( pkgs []string, vulns map[string][]types.Vulnerability, ) { for _, pkg := range pkgs { vlist := vulns[pkg] sortVulnsBySeverity(vlist) fmt.Printf( "\n %s %s %s\n", ui.Magenta(ui.Gem), ui.HiCyan(pkg), ui.HiBlack(fmt.Sprintf( "(%d %s: %s)", len(vlist), pluralize("vuln", len(vlist)), severityBreakdown(vlist), )), ) for _, v := range vlist { id := preferredID(v) sevColor := severityColorFn(v.Severity) fmt.Printf( "\n %s %s\n", sevColor( padRight(strings.ToUpper(v.Severity), 8), ), ui.Magenta(id), ) fmt.Printf( " %s\n", ui.HiBlack(truncate(v.Summary, 72)), ) if v.FixedIn != "" { fmt.Printf( " %s %s\n", ui.HiBlack("Fixed in:"), ui.Green(v.FixedIn), ) } if v.Link != "" { fmt.Printf( " %s\n", ui.BlueItalic(v.Link), ) } } } fmt.Println() } func printVulnLine(v types.Vulnerability) { id := preferredID(v) sevColor := severityColorFn(v.Severity) fixedStr := "" if v.FixedIn != "" { fixedStr = ui.HiBlack("Fixed: ") + ui.Green(v.FixedIn) } fmt.Printf( " %s %s %s %s\n", sevColor( padRight(strings.ToUpper(v.Severity), 8), ), ui.Blue(padRight(id, 16)), ui.HiBlack(truncate(v.Summary, 38)), fixedStr, ) } func sortedVulnPackages( vulns map[string][]types.Vulnerability, ) []string { pkgs := make([]string, 0, len(vulns)) for k := range vulns { pkgs = append(pkgs, k) } sort.Slice(pkgs, func(i, j int) bool { li := len(vulns[pkgs[i]]) lj := len(vulns[pkgs[j]]) if li != lj { return li > lj } return pkgs[i] < pkgs[j] }) return pkgs } func sortVulnsBySeverity( vulns []types.Vulnerability, ) { sort.Slice(vulns, func(i, j int) bool { ri := severityRank(vulns[i].Severity) rj := severityRank(vulns[j].Severity) if ri != rj { return ri < rj } return vulns[i].ID < vulns[j].ID }) } func severityRank(sev string) int { switch strings.ToUpper(sev) { case severityCritical: return 0 case severityHigh: return 1 case severityModerate: return 2 case severityLow: return 3 default: return 4 } } func severityBreakdown( vulns []types.Vulnerability, ) string { counts := make(map[string]int) for _, v := range vulns { counts[strings.ToUpper(v.Severity)]++ } order := []string{ severityCritical, severityHigh, severityModerate, severityLow, "UNKNOWN", } var parts []string for _, sev := range order { if n := counts[sev]; n > 0 { sevColor := severityColorFn(sev) parts = append(parts, sevColor(fmt.Sprintf( "%d %s", n, strings.ToLower(sev), ))) } } return strings.Join(parts, ui.HiBlack(", ")) } func preferredID(v types.Vulnerability) string { for _, alias := range v.Aliases { if strings.HasPrefix(alias, "CVE-") { return alias } } return v.ID } func truncate(s string, maxLen int) string { if len(s) <= maxLen { return s } return s[:maxLen-3] + "..." } func filterIgnoredVulns( vulns map[string][]types.Vulnerability, ignoreIDs []string, ) map[string][]types.Vulnerability { if len(ignoreIDs) == 0 { return vulns } ignored := make(map[string]bool, len(ignoreIDs)) for _, id := range ignoreIDs { ignored[id] = true } filtered := make(map[string][]types.Vulnerability) for pkg, vlist := range vulns { var kept []types.Vulnerability for _, v := range vlist { if isVulnIgnored(v, ignored) { continue } kept = append(kept, v) } if len(kept) > 0 { filtered[pkg] = kept } } return filtered } func isVulnIgnored( v types.Vulnerability, ignored map[string]bool, ) bool { if ignored[v.ID] { return true } for _, alias := range v.Aliases { if ignored[alias] { return true } } return false } func filterVulnsBySeverity( vulns map[string][]types.Vulnerability, minSev string, ) map[string][]types.Vulnerability { threshold := severityRank(minSev) filtered := make(map[string][]types.Vulnerability) for pkg, vlist := range vulns { var kept []types.Vulnerability for _, v := range vlist { if severityRank(v.Severity) <= threshold { kept = append(kept, v) } } if len(kept) > 0 { filtered[pkg] = kept } } return filtered } func PrintSummary( result types.ScanResult, updated bool, ) { printDivider() printSectionHeader( ui.Star, "Summary", ui.Green, ui.RedItalic, ) if updated { fmt.Printf( " %s %s\n", ui.HiGreen(ui.Check), ui.HiGreen("Updated pyproject.toml"), ) } fmt.Printf( " %s %s %s\n", ui.Green(ui.Check), ui.Green(fmt.Sprintf("%d", result.TotalPackages)), ui.GreenItalic("packages checked"), ) if result.TotalUpdated > 0 { fmt.Printf( " %s %s %s\n", ui.Green(ui.ArrowUp), ui.Green( fmt.Sprintf("%d", result.TotalUpdated), ), ui.GreenItalic("updated"), ) } if result.VulnsScanned { if result.TotalVulns > 0 { fmt.Printf( " %s %s %s\n", ui.HiRed(ui.Cross), ui.RedBold( fmt.Sprintf("%d", result.TotalVulns), ), ui.HiRed( pluralize( "vulnerability", result.TotalVulns, )+" found", ), ) } else { fmt.Printf( " %s %s\n", ui.HiGreen(ui.Check), ui.HiGreen("No vulnerabilities found"), ) } } fmt.Printf( " %s %s\n\n", ui.HiCyan(ui.Timer), ui.HiBlueItalic( result.Duration.Round(1e6).String(), ), ) } func PrintError(msg string) { fmt.Printf( "\n %s %s %s\n\n", ui.RedBold(ui.Cross), ui.RedBold("error:"), ui.HiRed(msg), ) } func changeColorFn( kind string, ) func(a ...any) string { switch kind { case pypi.Major.String(): return ui.Red case pypi.Minor.String(): return ui.HiYellow default: return ui.Green } } func severityColorFn( sev string, ) func(a ...any) string { switch strings.ToUpper(sev) { case severityCritical: return ui.Red case severityHigh: return ui.RedBold case severityModerate: return ui.HiYellow case severityLow: return ui.Cyan default: return ui.HiBlack } } func padRight(s string, width int) string { if len(s) >= width { return s } return s + strings.Repeat(" ", width-len(s)) } func pluralize(word string, n int) string { if n == 1 { return word } if strings.HasSuffix(word, "y") { return word[:len(word)-1] + "ies" } return word + "s" }