Skip to content

Commit e4f1345

Browse files
authored
feat: colorize skills audit findings and animate update check (#283)
- 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.
1 parent f7d9641 commit e4f1345

4 files changed

Lines changed: 82 additions & 9 deletions

File tree

cmd/root.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,12 @@ var updateCmd = &cobra.Command{
467467
if ver == "" {
468468
ver = "dev"
469469
}
470+
prog := NewCLIProgress("Update check", []string{"Checking GitHub for updates"})
471+
defer prog.Abort()
472+
prog.StartStep(0)
470473
release, err := update.Check(ver)
474+
prog.CompleteStep(0)
475+
prog.Done()
471476
if err != nil {
472477
cmd.Println(auditTint("Update check failed: "+err.Error(), errorCoral))
473478
return nil

cmd/skills_cmd.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ var skillsAuditCmd = &cobra.Command{
167167
fmt.Println(string(data))
168168
return nil
169169
}
170-
fmt.Println(plugin.FormatAuditResult(r))
170+
fmt.Println(plugin.FormatAuditResultColored(r))
171171
return nil
172172
}
173173
if _, path, ok := plugin.InstalledSkillInfo(target); ok {
@@ -178,7 +178,7 @@ var skillsAuditCmd = &cobra.Command{
178178
fmt.Println(string(data))
179179
return nil
180180
}
181-
fmt.Println(plugin.FormatAuditResult(r))
181+
fmt.Println(plugin.FormatAuditResultColored(r))
182182
return nil
183183
}
184184
return fmt.Errorf("skill or file %q not found", target)
@@ -189,7 +189,7 @@ var skillsAuditCmd = &cobra.Command{
189189
fmt.Println(string(data))
190190
return nil
191191
}
192-
fmt.Println(plugin.FormatAuditResult(result))
192+
fmt.Println(plugin.FormatAuditResultColored(result))
193193
return nil
194194
},
195195
}

internal/plugin/audit.go

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ package plugin
22

33
import (
44
"fmt"
5+
"image/color"
56
"io/fs"
67
"os"
78
"path/filepath"
89
"strings"
910
"unicode"
1011

12+
"github.com/GrayCodeAI/graycode-cli/internal/theme"
1113
"github.com/GrayCodeAI/graycode-cli/internal/ui/icons"
1214
)
1315

@@ -138,8 +140,31 @@ func AuditAllSkills() AuditResult {
138140

139141
// FormatAuditResult formats audit findings for display.
140142
func FormatAuditResult(r AuditResult) string {
143+
return formatAuditResult(r, false)
144+
}
145+
146+
// FormatAuditResultColored formats audit findings with semantic severity
147+
// colors for direct terminal display (e.g. `graycode skills audit`). Prefer
148+
// FormatAuditResult when embedding the result inside another styled surface
149+
// (e.g. chat system messages) to avoid nested ANSI codes.
150+
func FormatAuditResultColored(r AuditResult) string {
151+
return formatAuditResult(r, true)
152+
}
153+
154+
func formatAuditResult(r AuditResult, colored bool) string {
155+
sev := func(s AuditSeverity) string {
156+
label := fmt.Sprintf("[%s]", s)
157+
if !colored {
158+
return label
159+
}
160+
return theme.Tint(label, severityColor(s))
161+
}
141162
if len(r.Findings) == 0 && len(r.Validation) == 0 {
142-
return fmt.Sprintf("Scanned %d file(s). No security issues found. "+icons.CheckBold(), r.Files)
163+
ok := fmt.Sprintf("Scanned %d file(s). No security issues found. "+icons.CheckBold(), r.Files)
164+
if colored {
165+
ok = theme.Tint(ok, theme.ReportSuccess)
166+
}
167+
return ok
143168
}
144169

145170
var b strings.Builder
@@ -155,25 +180,50 @@ func FormatAuditResult(r AuditResult) string {
155180
case SeverityInfo:
156181
info++
157182
}
158-
_, _ = fmt.Fprintf(&b, " [%s] %s:%d:%d — %s\n", f.Severity, f.File, f.Line, f.Column, f.Message)
183+
_, _ = fmt.Fprintf(&b, " %s %s:%d:%d — %s\n", sev(f.Severity), f.File, f.Line, f.Column, f.Message)
159184
}
160185

161186
b.WriteString("\n")
162187
for _, f := range r.Validation {
163-
_, _ = fmt.Fprintf(&b, " [%s] %s — %s\n", f.Severity, f.Path, f.Message)
188+
_, _ = fmt.Fprintf(&b, " %s %s — %s\n", sev(f.Severity), f.Path, f.Message)
164189
}
165190
if critical > 0 {
166-
_, _ = fmt.Fprintf(&b, icons.Alert()+" %d CRITICAL finding(s) — these skills may contain hidden malicious content.\n", critical)
191+
line := icons.Alert() + fmt.Sprintf(" %d CRITICAL finding(s) — these skills may contain hidden malicious content.\n", critical)
192+
if colored {
193+
line = theme.Tint(line, theme.ReportError)
194+
}
195+
b.WriteString(line)
167196
}
168197
if warning > 0 {
169-
_, _ = fmt.Fprintf(&b, " %d WARNING(s) — invisible characters that may hide content.\n", warning)
198+
line := fmt.Sprintf(" %d WARNING(s) — invisible characters that may hide content.\n", warning)
199+
if colored {
200+
line = theme.Tint(line, theme.ReportWarn)
201+
}
202+
b.WriteString(line)
170203
}
171204
if info > 0 {
172-
_, _ = fmt.Fprintf(&b, " %d INFO — potential homoglyphs (may be legitimate non-Latin text).\n", info)
205+
line := fmt.Sprintf(" %d INFO — potential homoglyphs (may be legitimate non-Latin text).\n", info)
206+
if colored {
207+
line = theme.Tint(line, theme.ReportInfo)
208+
}
209+
b.WriteString(line)
173210
}
174211
return b.String()
175212
}
176213

214+
// severityColor maps an audit severity to its semantic report color.
215+
func severityColor(sev AuditSeverity) color.Color {
216+
switch sev {
217+
case SeverityCritical:
218+
return theme.ReportError
219+
case SeverityWarning:
220+
return theme.ReportWarn
221+
case SeverityInfo:
222+
return theme.ReportInfo
223+
}
224+
return theme.ReportMuted
225+
}
226+
177227
// StripDangerousChars removes dangerous Unicode characters from content.
178228
func StripDangerousChars(content string) string {
179229
var b strings.Builder

internal/plugin/auto_skill_audit_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,24 @@ func TestFormatAuditResultFindings(t *testing.T) {
212212
}
213213
}
214214

215+
func TestFormatAuditResultColored(t *testing.T) {
216+
t.Setenv("NO_COLOR", "")
217+
t.Setenv("FORCE_COLOR", "1")
218+
r := AuditResult{
219+
Files: 1,
220+
Findings: []AuditFinding{
221+
{File: "test.md", Line: 1, Column: 5, Severity: SeverityCritical, Category: "bidi-override", Message: "BiDi override (U+202E)"},
222+
},
223+
}
224+
out := FormatAuditResultColored(r)
225+
if !strings.Contains(out, "\x1b[") {
226+
t.Error("expected ANSI escape in colored output under FORCE_COLOR")
227+
}
228+
if plain := FormatAuditResult(r); strings.Contains(plain, "\x1b[") {
229+
t.Error("plain FormatAuditResult should not emit ANSI")
230+
}
231+
}
232+
215233
func TestStripDangerousChars(t *testing.T) {
216234
input := "Hello\u202E world\u200B end"
217235
result := StripDangerousChars(input)

0 commit comments

Comments
 (0)