From eeaa99513216ccb48eb440885666b2388f6943aa Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Mon, 7 Sep 2026 10:07:53 +0530 Subject: [PATCH] feat: colorize skills audit findings and animate update check - Add plugin.FormatAuditResultColored: semantic severity colors for [CRITICAL]/[WARNING]/[INFO] labels and the summary lines, honoring NO_COLOR/TTY detection via theme.Tint. Used by 'graycode skills audit'; the plain FormatAuditResult stays uncolored for embedding in chat system messages (avoids nested ANSI). Shared body via a colored bool. - Animate 'graycode update' with a CLIProgress spinner around the network check (10s HTTP timeout), matching the doctor/verify pattern. --- cmd/root.go | 5 ++ cmd/skills_cmd.go | 6 +-- internal/plugin/audit.go | 62 +++++++++++++++++++++--- internal/plugin/auto_skill_audit_test.go | 18 +++++++ 4 files changed, 82 insertions(+), 9 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 992312df..33cd4c98 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -467,7 +467,12 @@ var updateCmd = &cobra.Command{ if ver == "" { ver = "dev" } + prog := NewCLIProgress("Update check", []string{"Checking GitHub for updates"}) + defer prog.Abort() + prog.StartStep(0) release, err := update.Check(ver) + prog.CompleteStep(0) + prog.Done() if err != nil { cmd.Println(auditTint("Update check failed: "+err.Error(), errorCoral)) return nil diff --git a/cmd/skills_cmd.go b/cmd/skills_cmd.go index f2bff432..100d671c 100644 --- a/cmd/skills_cmd.go +++ b/cmd/skills_cmd.go @@ -167,7 +167,7 @@ var skillsAuditCmd = &cobra.Command{ fmt.Println(string(data)) return nil } - fmt.Println(plugin.FormatAuditResult(r)) + fmt.Println(plugin.FormatAuditResultColored(r)) return nil } if _, path, ok := plugin.InstalledSkillInfo(target); ok { @@ -178,7 +178,7 @@ var skillsAuditCmd = &cobra.Command{ fmt.Println(string(data)) return nil } - fmt.Println(plugin.FormatAuditResult(r)) + fmt.Println(plugin.FormatAuditResultColored(r)) return nil } return fmt.Errorf("skill or file %q not found", target) @@ -189,7 +189,7 @@ var skillsAuditCmd = &cobra.Command{ fmt.Println(string(data)) return nil } - fmt.Println(plugin.FormatAuditResult(result)) + fmt.Println(plugin.FormatAuditResultColored(result)) return nil }, } diff --git a/internal/plugin/audit.go b/internal/plugin/audit.go index d95241cb..f13a2636 100644 --- a/internal/plugin/audit.go +++ b/internal/plugin/audit.go @@ -2,12 +2,14 @@ package plugin import ( "fmt" + "image/color" "io/fs" "os" "path/filepath" "strings" "unicode" + "github.com/GrayCodeAI/graycode-cli/internal/theme" "github.com/GrayCodeAI/graycode-cli/internal/ui/icons" ) @@ -138,8 +140,31 @@ func AuditAllSkills() AuditResult { // FormatAuditResult formats audit findings for display. func FormatAuditResult(r AuditResult) string { + return formatAuditResult(r, false) +} + +// FormatAuditResultColored formats audit findings with semantic severity +// colors for direct terminal display (e.g. `graycode skills audit`). Prefer +// FormatAuditResult when embedding the result inside another styled surface +// (e.g. chat system messages) to avoid nested ANSI codes. +func FormatAuditResultColored(r AuditResult) string { + return formatAuditResult(r, true) +} + +func formatAuditResult(r AuditResult, colored bool) string { + sev := func(s AuditSeverity) string { + label := fmt.Sprintf("[%s]", s) + if !colored { + return label + } + return theme.Tint(label, severityColor(s)) + } if len(r.Findings) == 0 && len(r.Validation) == 0 { - return fmt.Sprintf("Scanned %d file(s). No security issues found. "+icons.CheckBold(), r.Files) + ok := fmt.Sprintf("Scanned %d file(s). No security issues found. "+icons.CheckBold(), r.Files) + if colored { + ok = theme.Tint(ok, theme.ReportSuccess) + } + return ok } var b strings.Builder @@ -155,25 +180,50 @@ func FormatAuditResult(r AuditResult) string { case SeverityInfo: info++ } - _, _ = fmt.Fprintf(&b, " [%s] %s:%d:%d — %s\n", f.Severity, f.File, f.Line, f.Column, f.Message) + _, _ = fmt.Fprintf(&b, " %s %s:%d:%d — %s\n", sev(f.Severity), f.File, f.Line, f.Column, f.Message) } b.WriteString("\n") for _, f := range r.Validation { - _, _ = fmt.Fprintf(&b, " [%s] %s — %s\n", f.Severity, f.Path, f.Message) + _, _ = fmt.Fprintf(&b, " %s %s — %s\n", sev(f.Severity), f.Path, f.Message) } if critical > 0 { - _, _ = fmt.Fprintf(&b, icons.Alert()+" %d CRITICAL finding(s) — these skills may contain hidden malicious content.\n", critical) + line := icons.Alert() + fmt.Sprintf(" %d CRITICAL finding(s) — these skills may contain hidden malicious content.\n", critical) + if colored { + line = theme.Tint(line, theme.ReportError) + } + b.WriteString(line) } if warning > 0 { - _, _ = fmt.Fprintf(&b, " %d WARNING(s) — invisible characters that may hide content.\n", warning) + line := fmt.Sprintf(" %d WARNING(s) — invisible characters that may hide content.\n", warning) + if colored { + line = theme.Tint(line, theme.ReportWarn) + } + b.WriteString(line) } if info > 0 { - _, _ = fmt.Fprintf(&b, " %d INFO — potential homoglyphs (may be legitimate non-Latin text).\n", info) + line := fmt.Sprintf(" %d INFO — potential homoglyphs (may be legitimate non-Latin text).\n", info) + if colored { + line = theme.Tint(line, theme.ReportInfo) + } + b.WriteString(line) } return b.String() } +// severityColor maps an audit severity to its semantic report color. +func severityColor(sev AuditSeverity) color.Color { + switch sev { + case SeverityCritical: + return theme.ReportError + case SeverityWarning: + return theme.ReportWarn + case SeverityInfo: + return theme.ReportInfo + } + return theme.ReportMuted +} + // StripDangerousChars removes dangerous Unicode characters from content. func StripDangerousChars(content string) string { var b strings.Builder diff --git a/internal/plugin/auto_skill_audit_test.go b/internal/plugin/auto_skill_audit_test.go index d0f9e1b9..21b6ad92 100644 --- a/internal/plugin/auto_skill_audit_test.go +++ b/internal/plugin/auto_skill_audit_test.go @@ -212,6 +212,24 @@ func TestFormatAuditResultFindings(t *testing.T) { } } +func TestFormatAuditResultColored(t *testing.T) { + t.Setenv("NO_COLOR", "") + t.Setenv("FORCE_COLOR", "1") + r := AuditResult{ + Files: 1, + Findings: []AuditFinding{ + {File: "test.md", Line: 1, Column: 5, Severity: SeverityCritical, Category: "bidi-override", Message: "BiDi override (U+202E)"}, + }, + } + out := FormatAuditResultColored(r) + if !strings.Contains(out, "\x1b[") { + t.Error("expected ANSI escape in colored output under FORCE_COLOR") + } + if plain := FormatAuditResult(r); strings.Contains(plain, "\x1b[") { + t.Error("plain FormatAuditResult should not emit ANSI") + } +} + func TestStripDangerousChars(t *testing.T) { input := "Hello\u202E world\u200B end" result := StripDangerousChars(input)