@tanagram/cli 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/tanagram CHANGED
Binary file
@@ -81,7 +81,7 @@ func checkFileWithLLM(ctx context.Context, file string, changes []git.ChangedLin
81
81
  File: file,
82
82
  LineNumber: firstLine.LineNumber,
83
83
  PolicyName: policy.Name,
84
- Message: fmt.Sprintf("%s\n\nLLM Analysis: %s", policy.Message, check.Reason),
84
+ Message: check.Reason,
85
85
  Code: formatChangesForDisplay(changes),
86
86
  })
87
87
  }
@@ -100,20 +100,10 @@ func formatChangesForLLM(changes []git.ChangedLine) string {
100
100
 
101
101
  // formatChangesForDisplay creates a compact display of changed lines
102
102
  func formatChangesForDisplay(changes []git.ChangedLine) string {
103
- if len(changes) == 1 {
103
+ // Just return the first line's content, trimmed
104
+ // Line numbers will be shown in the formatted output
105
+ if len(changes) > 0 {
104
106
  return strings.TrimSpace(changes[0].Content)
105
107
  }
106
-
107
- var builder strings.Builder
108
- for i, change := range changes {
109
- if i > 0 {
110
- builder.WriteString("\n")
111
- }
112
- builder.WriteString(fmt.Sprintf("L%d: %s", change.LineNumber, strings.TrimSpace(change.Content)))
113
- if i >= 2 { // Limit to first 3 lines
114
- builder.WriteString("\n...")
115
- break
116
- }
117
- }
118
- return builder.String()
108
+ return ""
119
109
  }
@@ -3,6 +3,7 @@ package checker
3
3
  import (
4
4
  "context"
5
5
  "fmt"
6
+ "sort"
6
7
  "strings"
7
8
 
8
9
  "github.com/tanagram/cli/git"
@@ -44,12 +45,46 @@ func FormatViolations(result *CheckResult) string {
44
45
  return "✓ No policy violations found"
45
46
  }
46
47
 
48
+ // ANSI color codes
49
+ const (
50
+ colorReset = "\033[0m"
51
+ colorGray = "\033[90m"
52
+ )
53
+
54
+ // Group violations by file
55
+ violationsByFile := make(map[string][]Violation)
56
+ for _, v := range result.Violations {
57
+ violationsByFile[v.File] = append(violationsByFile[v.File], v)
58
+ }
59
+
60
+ // Sort files for consistent output
61
+ files := make([]string, 0, len(violationsByFile))
62
+ for file := range violationsByFile {
63
+ files = append(files, file)
64
+ }
65
+ sort.Strings(files)
66
+
47
67
  var output strings.Builder
48
68
  output.WriteString(fmt.Sprintf("✗ Found %d policy violation(s):\n\n", len(result.Violations)))
49
69
 
50
- for _, v := range result.Violations {
51
- output.WriteString(fmt.Sprintf("%s:%d - [%s] %s\n", v.File, v.LineNumber, v.PolicyName, v.Message))
52
- output.WriteString(fmt.Sprintf(" > %s\n\n", v.Code))
70
+ for _, file := range files {
71
+ violations := violationsByFile[file]
72
+
73
+ // Sort violations by line number
74
+ sort.Slice(violations, func(i, j int) bool {
75
+ return violations[i].LineNumber < violations[j].LineNumber
76
+ })
77
+
78
+ output.WriteString(fmt.Sprintf("- %s\n\n", file))
79
+
80
+ for _, v := range violations {
81
+ // Print the code line in gray
82
+ output.WriteString(fmt.Sprintf(" %s%d: %s%s\n", colorGray, v.LineNumber, v.Code, colorReset))
83
+
84
+ // Print the violation message indented with arrow
85
+ output.WriteString(fmt.Sprintf(" ^ %s\n", v.Message))
86
+ }
87
+ output.WriteString("\n")
53
88
  }
54
89
 
55
90
  return output.String()
package/commands/run.go CHANGED
@@ -5,11 +5,13 @@ import (
5
5
  "fmt"
6
6
  "os"
7
7
  "path/filepath"
8
+ "sync"
8
9
  "time"
9
10
 
10
11
  "github.com/tanagram/cli/checker"
11
12
  "github.com/tanagram/cli/extractor"
12
13
  "github.com/tanagram/cli/git"
14
+ "github.com/tanagram/cli/parser"
13
15
  "github.com/tanagram/cli/storage"
14
16
  )
15
17
 
@@ -69,38 +71,93 @@ func Run() error {
69
71
 
70
72
  // Auto-sync if any files changed
71
73
  if needsSync {
72
- fmt.Printf("\nSyncing policies with LLM...\n")
74
+ fmt.Printf("\nSyncing policies with LLM (processing %d files in parallel)...\n", len(instructionFiles))
73
75
 
74
- totalPolicies := 0
75
76
  ctx := context.Background()
76
77
 
77
- for i, file := range instructionFiles {
78
- relPath, _ := filepath.Rel(gitRoot, file)
79
-
80
- // Start spinner
81
- stop := make(chan bool)
82
- go spinner(stop, fmt.Sprintf("Processing %s (%d/%d)", relPath, i+1, len(instructionFiles)))
78
+ // Result type for collecting sync results
79
+ type syncResult struct {
80
+ file string
81
+ relPath string
82
+ policies []parser.Policy
83
+ err error
84
+ }
83
85
 
84
- policies, err := extractor.ExtractPoliciesFromFile(ctx, file)
86
+ // Channel to collect results
87
+ results := make(chan syncResult, len(instructionFiles))
88
+ var wg sync.WaitGroup
89
+
90
+ // Start spinner
91
+ stop := make(chan bool)
92
+ var completed int
93
+ var mu sync.Mutex
94
+ go func() {
95
+ chars := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
96
+ i := 0
97
+ for {
98
+ select {
99
+ case <-stop:
100
+ fmt.Print("\r")
101
+ return
102
+ default:
103
+ mu.Lock()
104
+ c := completed
105
+ mu.Unlock()
106
+ fmt.Printf("\r%s Processing files... (%d/%d completed)", chars[i%len(chars)], c, len(instructionFiles))
107
+ i++
108
+ time.Sleep(100 * time.Millisecond)
109
+ }
110
+ }
111
+ }()
112
+
113
+ // Launch goroutines for each file
114
+ for _, file := range instructionFiles {
115
+ wg.Add(1)
116
+ go func(file string) {
117
+ defer wg.Done()
118
+ relPath, _ := filepath.Rel(gitRoot, file)
119
+ policies, err := extractor.ExtractPoliciesFromFile(ctx, file)
120
+ results <- syncResult{file, relPath, policies, err}
121
+ }(file)
122
+ }
85
123
 
86
- // Stop spinner
87
- stop <- true
88
- close(stop)
89
- time.Sleep(50 * time.Millisecond)
124
+ // Close results channel when all goroutines complete
125
+ go func() {
126
+ wg.Wait()
127
+ close(results)
128
+ }()
90
129
 
91
- if err != nil {
92
- fmt.Printf("\r✗ Failed to process %s\n", relPath)
93
- return fmt.Errorf("failed to extract policies from %s: %w", file, err)
130
+ // Collect results
131
+ totalPolicies := 0
132
+ for result := range results {
133
+ mu.Lock()
134
+ completed++
135
+ mu.Unlock()
136
+
137
+ if result.err != nil {
138
+ stop <- true
139
+ close(stop)
140
+ time.Sleep(50 * time.Millisecond)
141
+ fmt.Printf("\r✗ Failed to process %s\n", result.relPath)
142
+ return fmt.Errorf("failed to extract policies from %s: %w", result.file, result.err)
94
143
  }
95
144
 
96
- if err := cache.UpdateFile(file, policies); err != nil {
97
- return fmt.Errorf("failed to update cache for %s: %w", file, err)
145
+ if err := cache.UpdateFile(result.file, result.policies); err != nil {
146
+ stop <- true
147
+ close(stop)
148
+ time.Sleep(50 * time.Millisecond)
149
+ return fmt.Errorf("failed to update cache for %s: %w", result.file, err)
98
150
  }
99
151
 
100
- fmt.Printf("\r✓ %s - %d policies\n", relPath, len(policies))
101
- totalPolicies += len(policies)
152
+ totalPolicies += len(result.policies)
153
+ fmt.Printf("\r✓ %s - %d policies\n", result.relPath, len(result.policies))
102
154
  }
103
155
 
156
+ // Stop spinner
157
+ stop <- true
158
+ close(stop)
159
+ time.Sleep(50 * time.Millisecond)
160
+
104
161
  if err := cache.Save(); err != nil {
105
162
  return fmt.Errorf("failed to save cache: %w", err)
106
163
  }
package/commands/sync.go CHANGED
@@ -5,9 +5,11 @@ import (
5
5
  "fmt"
6
6
  "os"
7
7
  "path/filepath"
8
+ "sync"
8
9
  "time"
9
10
 
10
11
  "github.com/tanagram/cli/extractor"
12
+ "github.com/tanagram/cli/parser"
11
13
  "github.com/tanagram/cli/storage"
12
14
  )
13
15
 
@@ -37,39 +39,94 @@ func Sync() error {
37
39
  return fmt.Errorf("failed to load cache: %w", err)
38
40
  }
39
41
 
40
- // Parse and sync each file using LLM
41
- fmt.Printf("\nSyncing policies with LLM...\n")
42
+ // Parse and sync each file using LLM in parallel
43
+ fmt.Printf("\nSyncing policies with LLM (processing %d files in parallel)...\n", len(instructionFiles))
42
44
 
43
- totalPolicies := 0
44
45
  ctx := context.Background()
45
46
 
46
- for i, file := range instructionFiles {
47
- relPath, _ := filepath.Rel(gitRoot, file)
48
-
49
- // Start spinner
50
- stop := make(chan bool)
51
- go spinner(stop, fmt.Sprintf("Processing %s (%d/%d)", relPath, i+1, len(instructionFiles)))
47
+ // Result type for collecting sync results
48
+ type syncResult struct {
49
+ file string
50
+ relPath string
51
+ policies []parser.Policy
52
+ err error
53
+ }
52
54
 
53
- policies, err := extractor.ExtractPoliciesFromFile(ctx, file)
55
+ // Channel to collect results
56
+ results := make(chan syncResult, len(instructionFiles))
57
+ var wg sync.WaitGroup
58
+
59
+ // Start spinner
60
+ stop := make(chan bool)
61
+ var completed int
62
+ var mu sync.Mutex
63
+ go func() {
64
+ chars := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
65
+ i := 0
66
+ for {
67
+ select {
68
+ case <-stop:
69
+ fmt.Print("\r")
70
+ return
71
+ default:
72
+ mu.Lock()
73
+ c := completed
74
+ mu.Unlock()
75
+ fmt.Printf("\r%s Processing files... (%d/%d completed)", chars[i%len(chars)], c, len(instructionFiles))
76
+ i++
77
+ time.Sleep(100 * time.Millisecond)
78
+ }
79
+ }
80
+ }()
81
+
82
+ // Launch goroutines for each file
83
+ for _, file := range instructionFiles {
84
+ wg.Add(1)
85
+ go func(file string) {
86
+ defer wg.Done()
87
+ relPath, _ := filepath.Rel(gitRoot, file)
88
+ policies, err := extractor.ExtractPoliciesFromFile(ctx, file)
89
+ results <- syncResult{file, relPath, policies, err}
90
+ }(file)
91
+ }
54
92
 
55
- // Stop spinner
56
- stop <- true
57
- close(stop)
58
- time.Sleep(50 * time.Millisecond)
93
+ // Close results channel when all goroutines complete
94
+ go func() {
95
+ wg.Wait()
96
+ close(results)
97
+ }()
59
98
 
60
- if err != nil {
61
- fmt.Printf("\r✗ Failed to process %s\n", relPath)
62
- return fmt.Errorf("failed to extract policies from %s: %w", file, err)
99
+ // Collect results
100
+ totalPolicies := 0
101
+ for result := range results {
102
+ mu.Lock()
103
+ completed++
104
+ mu.Unlock()
105
+
106
+ if result.err != nil {
107
+ stop <- true
108
+ close(stop)
109
+ time.Sleep(50 * time.Millisecond)
110
+ fmt.Printf("\r✗ Failed to process %s\n", result.relPath)
111
+ return fmt.Errorf("failed to extract policies from %s: %w", result.file, result.err)
63
112
  }
64
113
 
65
- if err := cache.UpdateFile(file, policies); err != nil {
66
- return fmt.Errorf("failed to update cache for %s: %w", file, err)
114
+ if err := cache.UpdateFile(result.file, result.policies); err != nil {
115
+ stop <- true
116
+ close(stop)
117
+ time.Sleep(50 * time.Millisecond)
118
+ return fmt.Errorf("failed to update cache for %s: %w", result.file, err)
67
119
  }
68
120
 
69
- fmt.Printf("\r✓ %s - %d policies\n", relPath, len(policies))
70
- totalPolicies += len(policies)
121
+ totalPolicies += len(result.policies)
122
+ fmt.Printf("\r✓ %s - %d policies\n", result.relPath, len(result.policies))
71
123
  }
72
124
 
125
+ // Stop spinner
126
+ stop <- true
127
+ close(stop)
128
+ time.Sleep(50 * time.Millisecond)
129
+
73
130
  // Save cache
74
131
  if err := cache.Save(); err != nil {
75
132
  return fmt.Errorf("failed to save cache: %w", err)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanagram/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Tanagram - Catch sloppy code before it ships",
5
5
  "main": "index.js",
6
6
  "bin": {