@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 +0 -0
- package/checker/llm_integration.go +5 -15
- package/checker/matcher.go +38 -3
- package/commands/run.go +77 -20
- package/commands/sync.go +78 -21
- package/package.json +1 -1
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:
|
|
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
|
-
|
|
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
|
}
|
package/checker/matcher.go
CHANGED
|
@@ -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 _,
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
124
|
+
// Close results channel when all goroutines complete
|
|
125
|
+
go func() {
|
|
126
|
+
wg.Wait()
|
|
127
|
+
close(results)
|
|
128
|
+
}()
|
|
90
129
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
93
|
+
// Close results channel when all goroutines complete
|
|
94
|
+
go func() {
|
|
95
|
+
wg.Wait()
|
|
96
|
+
close(results)
|
|
97
|
+
}()
|
|
59
98
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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)
|