@tanagram/cli 0.4.11 → 0.4.14
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/README.md +80 -66
- package/api/client.go +12 -2
- package/api/client_test.go +53 -0
- package/commands/config.go +74 -83
- package/commands/config_test.go +185 -44
- package/commands/login.go +2 -2
- package/commands/run.go +21 -23
- package/commands/snapshot_test.go +3 -3
- package/go.mod +2 -1
- package/go.sum +4 -0
- package/main.go +112 -12
- package/package.json +1 -1
- package/utils/process.go +3 -2
package/commands/config_test.go
CHANGED
|
@@ -4,6 +4,7 @@ import (
|
|
|
4
4
|
"encoding/json"
|
|
5
5
|
"os"
|
|
6
6
|
"path/filepath"
|
|
7
|
+
"strings"
|
|
7
8
|
"testing"
|
|
8
9
|
)
|
|
9
10
|
|
|
@@ -31,12 +32,12 @@ func TestConfigClaude(t *testing.T) {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
settings := readSettings(t, settingsPath)
|
|
34
|
-
|
|
35
|
-
if !
|
|
36
|
-
t.Error("
|
|
35
|
+
sessionStart, stop := checkHooks(settings)
|
|
36
|
+
if !sessionStart {
|
|
37
|
+
t.Error("SessionStart hook not created")
|
|
37
38
|
}
|
|
38
|
-
if !
|
|
39
|
-
t.Error("
|
|
39
|
+
if !stop {
|
|
40
|
+
t.Error("Stop hook not created")
|
|
40
41
|
}
|
|
41
42
|
})
|
|
42
43
|
|
|
@@ -47,22 +48,22 @@ func TestConfigClaude(t *testing.T) {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
settings := readSettings(t, settingsPath)
|
|
50
|
-
|
|
51
|
-
if !
|
|
51
|
+
sessionStart, stop := checkHooks(settings)
|
|
52
|
+
if !sessionStart || !stop {
|
|
52
53
|
t.Error("Hooks missing after second run")
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
// Check that we didn't duplicate them (rudimentary check: count of items)
|
|
56
57
|
hooks := settings["hooks"].(map[string]interface{})
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
sessionStartHooks := hooks["SessionStart"].([]interface{})
|
|
59
|
+
stopHooks := hooks["Stop"].([]interface{})
|
|
59
60
|
|
|
60
61
|
// We expect exactly 1 hook in each since we started fresh
|
|
61
|
-
if len(
|
|
62
|
-
t.Errorf("Expected 1
|
|
62
|
+
if len(sessionStartHooks) != 1 {
|
|
63
|
+
t.Errorf("Expected 1 SessionStart hook, got %d", len(sessionStartHooks))
|
|
63
64
|
}
|
|
64
|
-
if len(
|
|
65
|
-
t.Errorf("Expected 1
|
|
65
|
+
if len(stopHooks) != 1 {
|
|
66
|
+
t.Errorf("Expected 1 Stop hook, got %d", len(stopHooks))
|
|
66
67
|
}
|
|
67
68
|
})
|
|
68
69
|
|
|
@@ -70,9 +71,9 @@ func TestConfigClaude(t *testing.T) {
|
|
|
70
71
|
// Reset with a file that has other hooks
|
|
71
72
|
otherHook := map[string]interface{}{
|
|
72
73
|
"hooks": map[string]interface{}{
|
|
73
|
-
"
|
|
74
|
+
"SessionStart": []interface{}{
|
|
74
75
|
map[string]interface{}{
|
|
75
|
-
"matcher": "
|
|
76
|
+
"matcher": "startup|clear",
|
|
76
77
|
"hooks": []interface{}{
|
|
77
78
|
map[string]interface{}{"command": "echo existing"},
|
|
78
79
|
},
|
|
@@ -100,19 +101,19 @@ func TestConfigClaude(t *testing.T) {
|
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
settings := readSettings(t, settingsPath)
|
|
103
|
-
|
|
104
|
-
if !
|
|
104
|
+
sessionStart, stop := checkHooks(settings)
|
|
105
|
+
if !sessionStart || !stop {
|
|
105
106
|
t.Error("Tanagram hooks missing when merging with existing config")
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
// Check if existing hook is still there
|
|
109
110
|
hooks := settings["hooks"].(map[string]interface{})
|
|
110
|
-
|
|
111
|
+
sessionStartHooks := hooks["SessionStart"].([]interface{})
|
|
111
112
|
|
|
112
113
|
foundExisting := false
|
|
113
|
-
for _, h := range
|
|
114
|
+
for _, h := range sessionStartHooks {
|
|
114
115
|
hMap := h.(map[string]interface{})
|
|
115
|
-
if hMap["matcher"] == "
|
|
116
|
+
if hMap["matcher"] == "startup|clear" {
|
|
116
117
|
// Check for the specific command to be sure it's the right one
|
|
117
118
|
if commands, ok := hMap["hooks"].([]interface{}); ok {
|
|
118
119
|
for _, cmd := range commands {
|
|
@@ -133,6 +134,85 @@ func TestConfigClaude(t *testing.T) {
|
|
|
133
134
|
t.Error("Existing hook was lost")
|
|
134
135
|
}
|
|
135
136
|
})
|
|
137
|
+
|
|
138
|
+
t.Run("Preserve existing absolute path hooks", func(t *testing.T) {
|
|
139
|
+
// Reset with a file that has absolute path hooks
|
|
140
|
+
absHook := map[string]interface{}{
|
|
141
|
+
"hooks": map[string]interface{}{
|
|
142
|
+
"SessionStart": []interface{}{
|
|
143
|
+
map[string]interface{}{
|
|
144
|
+
"matcher": "startup|clear",
|
|
145
|
+
"hooks": []interface{}{
|
|
146
|
+
map[string]interface{}{"command": "/absolute/path/to/tanagram snapshot"},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
"Stop": []interface{}{
|
|
151
|
+
map[string]interface{}{
|
|
152
|
+
"hooks": []interface{}{
|
|
153
|
+
map[string]interface{}{
|
|
154
|
+
"type": "command",
|
|
155
|
+
"command": "/absolute/path/to/tanagram",
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Clean up previous run
|
|
164
|
+
os.RemoveAll(filepath.Dir(settingsPath))
|
|
165
|
+
|
|
166
|
+
dir := filepath.Dir(settingsPath)
|
|
167
|
+
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
168
|
+
t.Fatalf("Failed to create dir: %v", err)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Write absHook to settingsPath
|
|
172
|
+
data, _ := json.Marshal(absHook)
|
|
173
|
+
if err := os.WriteFile(settingsPath, data, 0644); err != nil {
|
|
174
|
+
t.Fatalf("Failed to write settings: %v", err)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if err := ConfigClaude(settingsPath); err != nil {
|
|
178
|
+
t.Fatalf("ConfigClaude() failed: %v", err)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
settings := readSettings(t, settingsPath)
|
|
182
|
+
|
|
183
|
+
// Check duplication
|
|
184
|
+
hooks := settings["hooks"].(map[string]interface{})
|
|
185
|
+
sessionStartHooks := hooks["SessionStart"].([]interface{})
|
|
186
|
+
stopHooks := hooks["Stop"].([]interface{})
|
|
187
|
+
|
|
188
|
+
// We expect exactly 1 hook since we started with 1 and it should match
|
|
189
|
+
if len(sessionStartHooks) != 1 {
|
|
190
|
+
t.Errorf("Expected 1 SessionStart hook, got %d", len(sessionStartHooks))
|
|
191
|
+
}
|
|
192
|
+
if len(stopHooks) != 1 {
|
|
193
|
+
t.Errorf("Expected 1 Stop hook, got %d", len(stopHooks))
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Check content
|
|
197
|
+
h := sessionStartHooks[0].(map[string]interface{})
|
|
198
|
+
innerHooks := h["hooks"].([]interface{})
|
|
199
|
+
cmdMap := innerHooks[0].(map[string]interface{})
|
|
200
|
+
cmd := cmdMap["command"].(string)
|
|
201
|
+
|
|
202
|
+
if cmd != "/absolute/path/to/tanagram snapshot" {
|
|
203
|
+
t.Errorf("Expected command to be preserved as '/absolute/path/to/tanagram snapshot', got '%s'", cmd)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check post content
|
|
207
|
+
hPost := stopHooks[0].(map[string]interface{})
|
|
208
|
+
innerHooksPost := hPost["hooks"].([]interface{})
|
|
209
|
+
cmdMapPost := innerHooksPost[0].(map[string]interface{})
|
|
210
|
+
cmdPost := cmdMapPost["command"].(string)
|
|
211
|
+
|
|
212
|
+
if cmdPost != "/absolute/path/to/tanagram" {
|
|
213
|
+
t.Errorf("Expected command to be preserved as '/absolute/path/to/tanagram', got '%s'", cmdPost)
|
|
214
|
+
}
|
|
215
|
+
})
|
|
136
216
|
}
|
|
137
217
|
|
|
138
218
|
func TestConfigCursor(t *testing.T) {
|
|
@@ -243,6 +323,69 @@ func TestConfigCursor(t *testing.T) {
|
|
|
243
323
|
t.Error("Existing hook was lost")
|
|
244
324
|
}
|
|
245
325
|
})
|
|
326
|
+
|
|
327
|
+
t.Run("Preserve existing absolute path hooks", func(t *testing.T) {
|
|
328
|
+
// Reset with a file that has absolute path hooks
|
|
329
|
+
absHook := map[string]interface{}{
|
|
330
|
+
"hooks": map[string]interface{}{
|
|
331
|
+
"beforeSubmitPrompt": []interface{}{
|
|
332
|
+
map[string]interface{}{"command": "/absolute/path/to/tanagram snapshot"},
|
|
333
|
+
},
|
|
334
|
+
"stop": []interface{}{
|
|
335
|
+
map[string]interface{}{"command": "/absolute/path/to/tanagram"},
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Clean up previous run
|
|
341
|
+
os.RemoveAll(filepath.Dir(hooksPath))
|
|
342
|
+
|
|
343
|
+
dir := filepath.Dir(hooksPath)
|
|
344
|
+
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
345
|
+
t.Fatalf("Failed to create dir: %v", err)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Write absHook to hooksPath
|
|
349
|
+
data, _ := json.Marshal(absHook)
|
|
350
|
+
if err := os.WriteFile(hooksPath, data, 0644); err != nil {
|
|
351
|
+
t.Fatalf("Failed to write settings: %v", err)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if err := ConfigCursor(hooksPath); err != nil {
|
|
355
|
+
t.Fatalf("ConfigCursor() failed: %v", err)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
settings := readSettings(t, hooksPath)
|
|
359
|
+
|
|
360
|
+
// Check duplication
|
|
361
|
+
hooks := settings["hooks"].(map[string]interface{})
|
|
362
|
+
beforeSubmitHooks := hooks["beforeSubmitPrompt"].([]interface{})
|
|
363
|
+
stopHooks := hooks["stop"].([]interface{})
|
|
364
|
+
|
|
365
|
+
// We expect exactly 1 hook since we started with 1 and it should match
|
|
366
|
+
if len(beforeSubmitHooks) != 1 {
|
|
367
|
+
t.Errorf("Expected 1 beforeSubmitPrompt hook, got %d", len(beforeSubmitHooks))
|
|
368
|
+
}
|
|
369
|
+
if len(stopHooks) != 1 {
|
|
370
|
+
t.Errorf("Expected 1 stop hook, got %d", len(stopHooks))
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Check content
|
|
374
|
+
h := beforeSubmitHooks[0].(map[string]interface{})
|
|
375
|
+
cmd, _ := h["command"].(string)
|
|
376
|
+
|
|
377
|
+
if cmd != "/absolute/path/to/tanagram snapshot" {
|
|
378
|
+
t.Errorf("Expected command to be preserved as '/absolute/path/to/tanagram snapshot', got '%s'", cmd)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Check stop content
|
|
382
|
+
hStop := stopHooks[0].(map[string]interface{})
|
|
383
|
+
cmdStop, _ := hStop["command"].(string)
|
|
384
|
+
|
|
385
|
+
if cmdStop != "/absolute/path/to/tanagram" {
|
|
386
|
+
t.Errorf("Expected command to be preserved as '/absolute/path/to/tanagram', got '%s'", cmdStop)
|
|
387
|
+
}
|
|
388
|
+
})
|
|
246
389
|
}
|
|
247
390
|
|
|
248
391
|
func TestEnsureHooksConfigured(t *testing.T) {
|
|
@@ -277,12 +420,12 @@ func TestEnsureHooksConfigured(t *testing.T) {
|
|
|
277
420
|
// Check Claude settings
|
|
278
421
|
claudeSettingsPath := filepath.Join(tempHome, ".claude", "settings.json")
|
|
279
422
|
claudeSettings := readSettings(t, claudeSettingsPath)
|
|
280
|
-
|
|
281
|
-
if !
|
|
282
|
-
t.Error("
|
|
423
|
+
sessionStart, stop := checkHooks(claudeSettings)
|
|
424
|
+
if !sessionStart {
|
|
425
|
+
t.Error("SessionStart hook not created")
|
|
283
426
|
}
|
|
284
|
-
if !
|
|
285
|
-
t.Error("
|
|
427
|
+
if !stop {
|
|
428
|
+
t.Error("Stop hook not created")
|
|
286
429
|
}
|
|
287
430
|
|
|
288
431
|
// Check Cursor hooks
|
|
@@ -316,16 +459,16 @@ func checkHooks(settings map[string]interface{}) (bool, bool) {
|
|
|
316
459
|
return false, false
|
|
317
460
|
}
|
|
318
461
|
|
|
319
|
-
|
|
320
|
-
if
|
|
321
|
-
for _, h := range
|
|
462
|
+
sessionStartExists := false
|
|
463
|
+
if sessionStart, ok := hooks["SessionStart"].([]interface{}); ok {
|
|
464
|
+
for _, h := range sessionStart {
|
|
322
465
|
if hMap, ok := h.(map[string]interface{}); ok {
|
|
323
|
-
if hMap["matcher"] == "
|
|
466
|
+
if hMap["matcher"] == "startup|clear" {
|
|
324
467
|
if innerHooks, ok := hMap["hooks"].([]interface{}); ok {
|
|
325
468
|
for _, ih := range innerHooks {
|
|
326
469
|
if ihMap, ok := ih.(map[string]interface{}); ok {
|
|
327
|
-
if ihMap["command"]
|
|
328
|
-
|
|
470
|
+
if cmd, ok := ihMap["command"].(string); ok && strings.HasSuffix(cmd, "tanagram snapshot") {
|
|
471
|
+
sessionStartExists = true
|
|
329
472
|
}
|
|
330
473
|
}
|
|
331
474
|
}
|
|
@@ -335,17 +478,15 @@ func checkHooks(settings map[string]interface{}) (bool, bool) {
|
|
|
335
478
|
}
|
|
336
479
|
}
|
|
337
480
|
|
|
338
|
-
|
|
339
|
-
if
|
|
340
|
-
for _, h := range
|
|
481
|
+
stopExists := false
|
|
482
|
+
if stopHooks, ok := hooks["Stop"].([]interface{}); ok {
|
|
483
|
+
for _, h := range stopHooks {
|
|
341
484
|
if hMap, ok := h.(map[string]interface{}); ok {
|
|
342
|
-
if hMap["
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if
|
|
346
|
-
|
|
347
|
-
postExists = true
|
|
348
|
-
}
|
|
485
|
+
if innerHooks, ok := hMap["hooks"].([]interface{}); ok {
|
|
486
|
+
for _, ih := range innerHooks {
|
|
487
|
+
if ihMap, ok := ih.(map[string]interface{}); ok {
|
|
488
|
+
if cmd, ok := ihMap["command"].(string); ok && strings.HasSuffix(cmd, "tanagram") {
|
|
489
|
+
stopExists = true
|
|
349
490
|
}
|
|
350
491
|
}
|
|
351
492
|
}
|
|
@@ -354,7 +495,7 @@ func checkHooks(settings map[string]interface{}) (bool, bool) {
|
|
|
354
495
|
}
|
|
355
496
|
}
|
|
356
497
|
|
|
357
|
-
return
|
|
498
|
+
return sessionStartExists, stopExists
|
|
358
499
|
}
|
|
359
500
|
|
|
360
501
|
func checkCursorHooks(settings map[string]interface{}) (bool, bool) {
|
|
@@ -367,7 +508,7 @@ func checkCursorHooks(settings map[string]interface{}) (bool, bool) {
|
|
|
367
508
|
if beforeSubmit, ok := hooks["beforeSubmitPrompt"].([]interface{}); ok {
|
|
368
509
|
for _, h := range beforeSubmit {
|
|
369
510
|
if hMap, ok := h.(map[string]interface{}); ok {
|
|
370
|
-
if cmd, ok := hMap["command"].(string); ok && cmd
|
|
511
|
+
if cmd, ok := hMap["command"].(string); ok && strings.HasSuffix(cmd, "tanagram snapshot") {
|
|
371
512
|
beforeSubmitExists = true
|
|
372
513
|
}
|
|
373
514
|
}
|
|
@@ -378,7 +519,7 @@ func checkCursorHooks(settings map[string]interface{}) (bool, bool) {
|
|
|
378
519
|
if stop, ok := hooks["stop"].([]interface{}); ok {
|
|
379
520
|
for _, h := range stop {
|
|
380
521
|
if hMap, ok := h.(map[string]interface{}); ok {
|
|
381
|
-
if cmd, ok := hMap["command"].(string); ok && cmd
|
|
522
|
+
if cmd, ok := hMap["command"].(string); ok && strings.HasSuffix(cmd, "tanagram") {
|
|
382
523
|
stopExists = true
|
|
383
524
|
}
|
|
384
525
|
}
|
package/commands/login.go
CHANGED
|
@@ -19,7 +19,7 @@ import (
|
|
|
19
19
|
// getWebAppURL returns the web app URL based on environment
|
|
20
20
|
func getWebAppURL() string {
|
|
21
21
|
// Check environment variable first
|
|
22
|
-
if url := os.Getenv("
|
|
22
|
+
if url := os.Getenv("TANAGRAM_WEB_HOSTNAME"); url != "" {
|
|
23
23
|
return url
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -79,7 +79,7 @@ func Login() error {
|
|
|
79
79
|
|
|
80
80
|
// Send success message to browser
|
|
81
81
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
82
|
-
fmt.
|
|
82
|
+
fmt.Fprint(w, `
|
|
83
83
|
<!DOCTYPE html>
|
|
84
84
|
<html lang="en">
|
|
85
85
|
<head>
|
package/commands/run.go
CHANGED
|
@@ -3,6 +3,7 @@ package commands
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
5
|
"fmt"
|
|
6
|
+
"log/slog"
|
|
6
7
|
"os"
|
|
7
8
|
"path/filepath"
|
|
8
9
|
"sync"
|
|
@@ -26,10 +27,10 @@ func spinner(stop chan bool, message string) {
|
|
|
26
27
|
for {
|
|
27
28
|
select {
|
|
28
29
|
case <-stop:
|
|
29
|
-
|
|
30
|
+
slog.Info("\r")
|
|
30
31
|
return
|
|
31
32
|
default:
|
|
32
|
-
|
|
33
|
+
slog.Info("spinner", "char", chars[i%len(chars)], "message", message)
|
|
33
34
|
i++
|
|
34
35
|
time.Sleep(100 * time.Millisecond)
|
|
35
36
|
}
|
|
@@ -80,7 +81,7 @@ func Run() error {
|
|
|
80
81
|
return err
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
|
|
84
|
+
slog.Info("Syncing policies with LLM", "files_to_process", len(filesToSync))
|
|
84
85
|
|
|
85
86
|
syncStart := time.Now()
|
|
86
87
|
ctx := context.Background()
|
|
@@ -102,19 +103,16 @@ func Run() error {
|
|
|
102
103
|
var completed int
|
|
103
104
|
var mu sync.Mutex
|
|
104
105
|
go func() {
|
|
105
|
-
chars := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
|
|
106
|
-
i := 0
|
|
107
106
|
for {
|
|
108
107
|
select {
|
|
109
108
|
case <-stop:
|
|
110
|
-
|
|
109
|
+
slog.Info("spinner stopped")
|
|
111
110
|
return
|
|
112
111
|
default:
|
|
113
112
|
mu.Lock()
|
|
114
113
|
c := completed
|
|
115
114
|
mu.Unlock()
|
|
116
|
-
|
|
117
|
-
i++
|
|
115
|
+
slog.Info("Processing files", "completed", c, "total", len(filesToSync))
|
|
118
116
|
time.Sleep(100 * time.Millisecond)
|
|
119
117
|
}
|
|
120
118
|
}
|
|
@@ -145,7 +143,7 @@ func Run() error {
|
|
|
145
143
|
close(stop)
|
|
146
144
|
time.Sleep(50 * time.Millisecond)
|
|
147
145
|
mu.Lock()
|
|
148
|
-
|
|
146
|
+
slog.Error("Failed to process file", "file", result.relPath)
|
|
149
147
|
mu.Unlock()
|
|
150
148
|
return fmt.Errorf("failed to extract policies from %s: %w", result.file, result.err)
|
|
151
149
|
}
|
|
@@ -162,7 +160,7 @@ func Run() error {
|
|
|
162
160
|
// Atomic update of counter and output (prevents race with spinner)
|
|
163
161
|
mu.Lock()
|
|
164
162
|
completed++
|
|
165
|
-
|
|
163
|
+
slog.Info("Processed file", "file", result.relPath, "policies", len(result.policies))
|
|
166
164
|
mu.Unlock()
|
|
167
165
|
}
|
|
168
166
|
|
|
@@ -176,7 +174,7 @@ func Run() error {
|
|
|
176
174
|
}
|
|
177
175
|
|
|
178
176
|
syncDuration := time.Since(syncStart)
|
|
179
|
-
|
|
177
|
+
slog.Info("Sync complete", "policies_synced", totalPolicies, "files_synced", len(filesToSync))
|
|
180
178
|
|
|
181
179
|
// Track sync metrics
|
|
182
180
|
metrics.Track("cli.sync.complete", map[string]interface{}{
|
|
@@ -202,10 +200,10 @@ func Run() error {
|
|
|
202
200
|
cloudPolicies, err = cloudStorage.LoadCloudPoliciesAsParserFormat(repoInfo.Owner, repoInfo.Name)
|
|
203
201
|
if err != nil {
|
|
204
202
|
// Cloud policies exist but failed to load - warn but continue
|
|
205
|
-
|
|
203
|
+
slog.Warn("Failed to load cloud policies", "error", err)
|
|
206
204
|
cloudPolicies = []parser.Policy{}
|
|
207
205
|
} else if len(cloudPolicies) > 0 {
|
|
208
|
-
|
|
206
|
+
slog.Info("Loaded cloud policies", "count", len(cloudPolicies), "owner", repoInfo.Owner, "repo", repoInfo.Name)
|
|
209
207
|
}
|
|
210
208
|
}
|
|
211
209
|
// If repo detection failed, silently continue with local-only policies
|
|
@@ -214,7 +212,7 @@ func Run() error {
|
|
|
214
212
|
policies := storage.MergePolicies(localPolicies, cloudPolicies)
|
|
215
213
|
|
|
216
214
|
if len(policies) == 0 {
|
|
217
|
-
|
|
215
|
+
slog.Info("No enforceable policies found")
|
|
218
216
|
return nil
|
|
219
217
|
}
|
|
220
218
|
|
|
@@ -223,9 +221,9 @@ func Run() error {
|
|
|
223
221
|
totalMerged := len(policies)
|
|
224
222
|
|
|
225
223
|
if totalCloud > 0 {
|
|
226
|
-
|
|
224
|
+
slog.Info("Total policies", "total", totalMerged, "local", totalLocal, "cloud", totalCloud, "after_merge", totalMerged)
|
|
227
225
|
} else {
|
|
228
|
-
|
|
226
|
+
slog.Info("Loaded local policies", "count", totalLocal)
|
|
229
227
|
}
|
|
230
228
|
|
|
231
229
|
// Check if a snapshot exists (from PreToolUse hook)
|
|
@@ -233,7 +231,7 @@ func Run() error {
|
|
|
233
231
|
useSnapshot := false
|
|
234
232
|
|
|
235
233
|
if snapshot.Exists(gitRoot) {
|
|
236
|
-
|
|
234
|
+
slog.Info("Snapshot detected - checking only Claude's changes")
|
|
237
235
|
|
|
238
236
|
// Load snapshot
|
|
239
237
|
snap, err := snapshot.Load(gitRoot)
|
|
@@ -265,13 +263,13 @@ func Run() error {
|
|
|
265
263
|
|
|
266
264
|
// Delete snapshot after using it
|
|
267
265
|
if err := snapshot.Delete(gitRoot); err != nil {
|
|
268
|
-
|
|
266
|
+
slog.Warn("Failed to delete snapshot", "error", err)
|
|
269
267
|
}
|
|
270
268
|
|
|
271
269
|
useSnapshot = true
|
|
272
270
|
} else {
|
|
273
271
|
// No snapshot - fall back to checking all git changes
|
|
274
|
-
|
|
272
|
+
slog.Info("Checking all changes (unstaged + staged)")
|
|
275
273
|
diffResult, err := gitpkg.GetAllChanges()
|
|
276
274
|
if err != nil {
|
|
277
275
|
return fmt.Errorf("error getting git diff: %w", err)
|
|
@@ -282,14 +280,14 @@ func Run() error {
|
|
|
282
280
|
|
|
283
281
|
if len(changesToCheck) == 0 {
|
|
284
282
|
if useSnapshot {
|
|
285
|
-
fmt.
|
|
283
|
+
fmt.Fprint(os.Stdout, "No changes detected since snapshot\n")
|
|
286
284
|
} else {
|
|
287
|
-
fmt.
|
|
285
|
+
fmt.Fprint(os.Stdout, "No changes to check\n")
|
|
288
286
|
}
|
|
289
287
|
return nil
|
|
290
288
|
}
|
|
291
289
|
|
|
292
|
-
|
|
290
|
+
slog.Info("Scanning changed lines", "count", len(changesToCheck))
|
|
293
291
|
|
|
294
292
|
// Get API key once upfront before checking
|
|
295
293
|
apiKey, err := config.GetAPIKey()
|
|
@@ -337,6 +335,6 @@ func Run() error {
|
|
|
337
335
|
}
|
|
338
336
|
|
|
339
337
|
// No violations found - output success message
|
|
340
|
-
fmt.
|
|
338
|
+
fmt.Fprint(os.Stdout, "No policy violations found")
|
|
341
339
|
return nil
|
|
342
340
|
}
|
|
@@ -10,11 +10,11 @@ import (
|
|
|
10
10
|
func TestSnapshot_Cursor(t *testing.T) {
|
|
11
11
|
// Save original functions
|
|
12
12
|
origFindGitRoot := findGitRoot
|
|
13
|
-
origCreateSnapshot :=
|
|
13
|
+
origCreateSnapshot := createOptimized
|
|
14
14
|
origGetParentProcess := getParentProcess
|
|
15
15
|
defer func() {
|
|
16
16
|
findGitRoot = origFindGitRoot
|
|
17
|
-
|
|
17
|
+
createOptimized = origCreateSnapshot
|
|
18
18
|
getParentProcess = origGetParentProcess
|
|
19
19
|
}()
|
|
20
20
|
|
|
@@ -22,7 +22,7 @@ func TestSnapshot_Cursor(t *testing.T) {
|
|
|
22
22
|
findGitRoot = func() (string, error) {
|
|
23
23
|
return "/tmp/mock-git-root", nil
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
createOptimized = func(root string, files []string) error {
|
|
26
26
|
return nil
|
|
27
27
|
}
|
|
28
28
|
|
package/go.mod
CHANGED
|
@@ -43,6 +43,7 @@ require (
|
|
|
43
43
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
|
44
44
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
|
45
45
|
github.com/zalando/go-keyring v0.2.6 // indirect
|
|
46
|
-
golang.org/x/sys v0.
|
|
46
|
+
golang.org/x/sys v0.38.0 // indirect
|
|
47
|
+
golang.org/x/term v0.37.0 // indirect
|
|
47
48
|
golang.org/x/text v0.27.0 // indirect
|
|
48
49
|
)
|
package/go.sum
CHANGED
|
@@ -97,6 +97,10 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
|
97
97
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
98
98
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
|
99
99
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
100
|
+
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
|
101
|
+
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
102
|
+
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
|
103
|
+
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
|
100
104
|
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
|
101
105
|
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
|
102
106
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|