@@ -3,12 +3,14 @@ package cmd
33import (
44 "encoding/json"
55 "fmt"
6+ "image/color"
67 "os"
78 "path/filepath"
89 "sort"
910 "strings"
1011 "time"
1112
13+ lipgloss "charm.land/lipgloss/v2"
1214 "github.com/GrayCodeAI/graycode-cli/internal/hooks/audit"
1315 "github.com/GrayCodeAI/graycode-cli/internal/storage"
1416 "github.com/spf13/cobra"
@@ -65,6 +67,13 @@ type AuditResult struct {
6567 Detectors []AuditCount `json:"detectors"`
6668}
6769
70+ // auditProgressEnabled reports whether per-session progress should be shown.
71+ // JSON output must stay pure (progress lines would corrupt it) and piped
72+ // text should not be spammed with per-session lines.
73+ func auditProgressEnabled (format string , tty bool ) bool {
74+ return format != "json" && tty
75+ }
76+
6877func runAudit (cmd * cobra.Command , args []string ) error {
6978 if auditJSON {
7079 auditFormat = "json"
@@ -77,18 +86,37 @@ func runAudit(cmd *cobra.Command, args []string) error {
7786 }
7887
7988 if len (sessions ) == 0 {
80- cmd .Println ("No session transcripts found for the specified time period." )
89+ cmd .Println (auditTint ( "No session transcripts found for the specified time period." , textMuted ) )
8190 return nil
8291 }
8392
93+ // Animated per-session progress only on a TTY and only for text output.
94+ // JSON output must stay pure (progress lines would corrupt it) and piped
95+ // text should not be spammed with per-session lines.
96+ var prog * CLIProgress
97+ if auditProgressEnabled (auditFormat , stdoutIsTerminal ()) && ! IsQuiet () {
98+ names := make ([]string , len (sessions ))
99+ for i := range sessions {
100+ names [i ] = fmt .Sprintf ("Scanning session %d/%d" , i + 1 , len (sessions ))
101+ }
102+ prog = NewCLIProgress ("Audit" , names )
103+ defer prog .Abort ()
104+ }
105+
84106 // Run audit detectors on each session
85107 detectors := audit .AllDetectors ()
86108 counts := make (map [string ]* AuditCount )
87109 totalHits := 0
88110
89- for _ , sess := range sessions {
111+ for i , sess := range sessions {
112+ if prog != nil {
113+ prog .StartStep (i )
114+ }
90115 events , err := loadSessionEvents (sess .Path )
91116 if err != nil {
117+ if prog != nil {
118+ prog .FailStep (i , "load failed" )
119+ }
92120 continue
93121 }
94122
@@ -125,6 +153,12 @@ func runAudit(cmd *cobra.Command, args []string) error {
125153 }
126154 }
127155 }
156+ if prog != nil {
157+ prog .CompleteStep (i )
158+ }
159+ }
160+ if prog != nil {
161+ prog .Done ()
128162 }
129163
130164 // Count unique projects per detector
@@ -250,25 +284,54 @@ func loadSessionEvents(path string) ([]audit.ToolEvent, error) {
250284 return events , nil
251285}
252286
287+ // auditTint applies a theme foreground color when color output is appropriate
288+ // (honors --quiet, NO_COLOR, FORCE_COLOR, and TTY state via ShouldColor).
289+ // Piped output stays plain so scripts never see stray ANSI escapes.
290+ func auditTint (s string , color color.Color ) string {
291+ if ! ShouldColor () || s == "" {
292+ return s
293+ }
294+ return lipgloss .NewStyle ().Foreground (color ).Render (s )
295+ }
296+
297+ // auditSeverityColor maps a detector severity to a semantic theme color.
298+ func auditSeverityColor (sev string ) color.Color {
299+ switch sev {
300+ case "high" , "critical" :
301+ return errorCoral
302+ case "medium" :
303+ return warnAmber
304+ default : // info, low
305+ return infoSky
306+ }
307+ }
308+
253309func printAuditText (cmd * cobra.Command , result AuditResult ) {
254310 w := cmd .OutOrStdout ()
255311
256312 _ , _ = fmt .Fprintf (w , "\n " )
257313 _ , _ = fmt .Fprintf (w , "═══════════════════════════════════════════════════════════════\n " )
258- _ , _ = fmt .Fprintf (w , " Graycode Audit Report\n " )
314+ _ , _ = fmt .Fprintf (w , " %s \n " , auditTint ( " Graycode Audit Report" , graycodeColor ) )
259315 _ , _ = fmt .Fprintf (w , "═══════════════════════════════════════════════════════════════\n " )
260316 _ , _ = fmt .Fprintf (w , "\n " )
261- _ , _ = fmt .Fprintf (w , " Scanned: %d sessions (last %d days)\n " , result .Sessions , result .Days )
262- _ , _ = fmt .Fprintf (w , " Total hits: %d\n " , result .TotalHits )
263- _ , _ = fmt .Fprintf (w , " Scanned at: %s\n " , result .ScannedAt )
317+ _ , _ = fmt .Fprintf (w , " %s %d sessions (last %d days)\n " ,
318+ auditTint ("Scanned:" , textMuted ), result .Sessions , result .Days )
319+ hitsColor := doneGreen
320+ if result .TotalHits > 0 {
321+ hitsColor = errorCoral
322+ }
323+ _ , _ = fmt .Fprintf (w , " %s %s\n " ,
324+ auditTint ("Total hits:" , textMuted ), auditTint (fmt .Sprintf ("%d" , result .TotalHits ), hitsColor ))
325+ _ , _ = fmt .Fprintf (w , " %s %s\n " ,
326+ auditTint ("Scanned at:" , textMuted ), auditTint (result .ScannedAt , textPrimary ))
264327
265328 if len (result .Detectors ) == 0 {
266- _ , _ = fmt .Fprintf (w , "\n No wasteful patterns detected. Great job!\n \n " )
329+ _ , _ = fmt .Fprintf (w , "\n %s \n \n " , auditTint ( " No wasteful patterns detected. Great job!" , doneGreen ) )
267330 return
268331 }
269332
270333 _ , _ = fmt .Fprintf (w , "\n " )
271- _ , _ = fmt .Fprintf (w , "─── Detected Patterns ───\n \n " )
334+ _ , _ = fmt .Fprintf (w , "─── %s ───\n \n " , auditTint ( "Detected Patterns" , infoSky ) )
272335 _ , _ = fmt .Fprintf (w , " %-30s %6s %8s %s\n " , "DETECTOR" , "HITS" , "SEVERITY" , "EXAMPLE" )
273336 _ , _ = fmt .Fprintf (w , " %-30s %6s %8s %s\n " , strings .Repeat ("─" , 30 ), strings .Repeat ("─" , 6 ), strings .Repeat ("─" , 8 ), strings .Repeat ("─" , 30 ))
274337
@@ -277,11 +340,14 @@ func printAuditText(cmd *cobra.Command, result AuditResult) {
277340 if len (d .Examples ) > 0 {
278341 example = d .Examples [0 ]
279342 }
280- _ , _ = fmt .Fprintf (w , " %-30s %6d %8s %s\n " , d .Name , d .Hits , d .Severity , example )
343+ // Pad to the column width first, then colorize, so ANSI escapes
344+ // (zero-width) don't break the fixed-width alignment.
345+ sev := auditTint (fmt .Sprintf ("%8s" , d .Severity ), auditSeverityColor (d .Severity ))
346+ _ , _ = fmt .Fprintf (w , " %-30s %6d %s %s\n " , d .Name , d .Hits , sev , example )
281347 }
282348
283349 _ , _ = fmt .Fprintf (w , "\n " )
284- _ , _ = fmt .Fprintf (w , "─── Remediation Tips ───\n \n " )
350+ _ , _ = fmt .Fprintf (w , "─── %s ───\n \n " , auditTint ( "Remediation Tips" , infoSky ) )
285351
286352 for _ , d := range result .Detectors {
287353 switch d .Name {
0 commit comments