Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions cmd/skills_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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
},
}
Expand Down
62 changes: 56 additions & 6 deletions internal/plugin/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
18 changes: 18 additions & 0 deletions internal/plugin/auto_skill_audit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading