@tanagram/cli 0.4.2 → 0.4.4
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/checker/matcher.go +64 -0
- package/commands/config.go +343 -70
- package/commands/config_test.go +389 -0
- package/commands/run.go +23 -19
- package/commands/snapshot.go +15 -3
- package/commands/snapshot_test.go +88 -0
- package/main.go +58 -8
- package/package.json +1 -1
- package/utils/process.go +15 -4
package/checker/matcher.go
CHANGED
|
@@ -2,7 +2,9 @@ package checker
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
|
+
"encoding/json"
|
|
5
6
|
"fmt"
|
|
7
|
+
"os"
|
|
6
8
|
"sort"
|
|
7
9
|
"strings"
|
|
8
10
|
|
|
@@ -138,3 +140,65 @@ func FormatClaudeInstructions(result *CheckResult) string {
|
|
|
138
140
|
|
|
139
141
|
return output.String()
|
|
140
142
|
}
|
|
143
|
+
|
|
144
|
+
// FormatCursorInstructions formats violations as a JSON response for Cursor
|
|
145
|
+
// structured as {"followup_message": "<text>"}
|
|
146
|
+
func FormatCursorInstructions(result *CheckResult) string {
|
|
147
|
+
if len(result.Violations) == 0 {
|
|
148
|
+
return ""
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Group violations by file
|
|
152
|
+
violationsByFile := make(map[string][]Violation)
|
|
153
|
+
for _, v := range result.Violations {
|
|
154
|
+
violationsByFile[v.File] = append(violationsByFile[v.File], v)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Sort files for consistent output
|
|
158
|
+
files := make([]string, 0, len(violationsByFile))
|
|
159
|
+
for file := range violationsByFile {
|
|
160
|
+
files = append(files, file)
|
|
161
|
+
}
|
|
162
|
+
sort.Strings(files)
|
|
163
|
+
|
|
164
|
+
var message strings.Builder
|
|
165
|
+
message.WriteString("POLICY VIOLATIONS DETECTED - PLEASE FIX\n\n")
|
|
166
|
+
message.WriteString(fmt.Sprintf("Found %d policy violation(s) that need to be fixed:\n\n", len(result.Violations)))
|
|
167
|
+
|
|
168
|
+
for _, file := range files {
|
|
169
|
+
violations := violationsByFile[file]
|
|
170
|
+
|
|
171
|
+
// Sort violations by line number
|
|
172
|
+
sort.Slice(violations, func(i, j int) bool {
|
|
173
|
+
return violations[i].LineNumber < violations[j].LineNumber
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
message.WriteString(fmt.Sprintf("File: %s\n", file))
|
|
177
|
+
|
|
178
|
+
for _, v := range violations {
|
|
179
|
+
message.WriteString(fmt.Sprintf("\n Line %d:\n", v.LineNumber))
|
|
180
|
+
message.WriteString(fmt.Sprintf(" Code: %s\n", v.Code))
|
|
181
|
+
message.WriteString(fmt.Sprintf(" Policy: %s\n", v.PolicyName))
|
|
182
|
+
message.WriteString(fmt.Sprintf(" Issue: %s\n", v.Message))
|
|
183
|
+
message.WriteString(fmt.Sprintf(" Action: Review and fix this code to comply with the policy.\n"))
|
|
184
|
+
}
|
|
185
|
+
message.WriteString("\n")
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
message.WriteString("Please fix all violations listed above and ensure the code complies with all policies.\n")
|
|
189
|
+
|
|
190
|
+
// Wrap in JSON structure
|
|
191
|
+
response := struct {
|
|
192
|
+
FollowupMessage string `json:"followup_message"`
|
|
193
|
+
}{
|
|
194
|
+
FollowupMessage: message.String(),
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
bytes, err := json.MarshalIndent(response, "", " ")
|
|
198
|
+
if err != nil {
|
|
199
|
+
fmt.Fprintf(os.Stderr, "Error formatting violations: %v\n", err)
|
|
200
|
+
return "{}"
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return string(bytes)
|
|
204
|
+
}
|
package/commands/config.go
CHANGED
|
@@ -8,34 +8,11 @@ import (
|
|
|
8
8
|
"strings"
|
|
9
9
|
)
|
|
10
10
|
|
|
11
|
-
// ConfigClaude sets up the Claude Code hook in
|
|
12
|
-
func ConfigClaude() error {
|
|
13
|
-
|
|
14
|
-
home, err := os.UserHomeDir()
|
|
11
|
+
// ConfigClaude sets up the Claude Code hook in the specified settings file
|
|
12
|
+
func ConfigClaude(settingsPath string) error {
|
|
13
|
+
settings, err := loadConfig(settingsPath)
|
|
15
14
|
if err != nil {
|
|
16
|
-
return fmt.Errorf("failed to
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
settingsPath := filepath.Join(home, ".claude", "settings.json")
|
|
20
|
-
|
|
21
|
-
// Read existing settings or create new structure
|
|
22
|
-
var settings map[string]interface{}
|
|
23
|
-
|
|
24
|
-
// Check if file exists
|
|
25
|
-
if _, err := os.Stat(settingsPath); os.IsNotExist(err) {
|
|
26
|
-
// File doesn't exist, create new settings
|
|
27
|
-
settings = make(map[string]interface{})
|
|
28
|
-
} else {
|
|
29
|
-
// File exists, read it
|
|
30
|
-
data, err := os.ReadFile(settingsPath)
|
|
31
|
-
if err != nil {
|
|
32
|
-
return fmt.Errorf("failed to read Claude settings: %w", err)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Parse JSON
|
|
36
|
-
if err := json.Unmarshal(data, &settings); err != nil {
|
|
37
|
-
return fmt.Errorf("failed to parse Claude settings (invalid JSON): %w", err)
|
|
38
|
-
}
|
|
15
|
+
return fmt.Errorf("failed to load Claude settings: %w", err)
|
|
39
16
|
}
|
|
40
17
|
|
|
41
18
|
// Check if hooks already exist
|
|
@@ -102,6 +79,7 @@ func ConfigClaude() error {
|
|
|
102
79
|
}
|
|
103
80
|
}
|
|
104
81
|
}
|
|
82
|
+
|
|
105
83
|
}
|
|
106
84
|
}
|
|
107
85
|
|
|
@@ -138,20 +116,8 @@ func ConfigClaude() error {
|
|
|
138
116
|
hooks["PostToolUse"] = postToolUse
|
|
139
117
|
}
|
|
140
118
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if err := os.MkdirAll(claudeDir, 0755); err != nil {
|
|
144
|
-
return fmt.Errorf("failed to create .claude directory: %w", err)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Write updated settings back to file
|
|
148
|
-
data, err := json.MarshalIndent(settings, "", " ")
|
|
149
|
-
if err != nil {
|
|
150
|
-
return fmt.Errorf("failed to marshal settings: %w", err)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if err := os.WriteFile(settingsPath, data, 0644); err != nil {
|
|
154
|
-
return fmt.Errorf("failed to write settings: %w", err)
|
|
119
|
+
if err := saveConfig(settingsPath, settings); err != nil {
|
|
120
|
+
return fmt.Errorf("failed to save settings: %w", err)
|
|
155
121
|
}
|
|
156
122
|
|
|
157
123
|
fmt.Println("✓ Tanagram hooks added to ~/.claude/settings.json")
|
|
@@ -164,35 +130,140 @@ func ConfigClaude() error {
|
|
|
164
130
|
return nil
|
|
165
131
|
}
|
|
166
132
|
|
|
167
|
-
//
|
|
168
|
-
func
|
|
169
|
-
|
|
133
|
+
// ConfigCursor sets up the Cursor hook in the specified settings file
|
|
134
|
+
func ConfigCursor(hooksPath string) error {
|
|
135
|
+
|
|
136
|
+
hooksConfig, err := loadConfig(hooksPath)
|
|
170
137
|
if err != nil {
|
|
171
|
-
return fmt.Errorf("failed to
|
|
138
|
+
return fmt.Errorf("failed to load Cursor hooks: %w", err)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Ensure version field exists
|
|
142
|
+
if _, ok := hooksConfig["version"]; !ok {
|
|
143
|
+
hooksConfig["version"] = 1
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check if hooks object exists
|
|
147
|
+
hooks, hooksExist := hooksConfig["hooks"].(map[string]interface{})
|
|
148
|
+
if !hooksExist {
|
|
149
|
+
hooks = make(map[string]interface{})
|
|
150
|
+
hooksConfig["hooks"] = hooks
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check if beforeSubmitPrompt exists
|
|
154
|
+
beforeSubmitPrompt, beforeSubmitExists := hooks["beforeSubmitPrompt"].([]interface{})
|
|
155
|
+
if !beforeSubmitExists {
|
|
156
|
+
beforeSubmitPrompt = []interface{}{}
|
|
172
157
|
}
|
|
173
158
|
|
|
159
|
+
// Check if stop exists
|
|
160
|
+
stopHooks, stopExists := hooks["stop"].([]interface{})
|
|
161
|
+
if !stopExists {
|
|
162
|
+
stopHooks = []interface{}{}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Check if tanagram hooks already exist
|
|
166
|
+
beforeSubmitHookExists := false
|
|
167
|
+
stopHookExists := false
|
|
168
|
+
|
|
169
|
+
for _, hook := range beforeSubmitPrompt {
|
|
170
|
+
hookMap, ok := hook.(map[string]interface{})
|
|
171
|
+
if !ok {
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if cmd, ok := hookMap["command"].(string); ok && cmd == "tanagram snapshot" {
|
|
176
|
+
beforeSubmitHookExists = true
|
|
177
|
+
break
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for _, hook := range stopHooks {
|
|
182
|
+
hookMap, ok := hook.(map[string]interface{})
|
|
183
|
+
if !ok {
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if cmd, ok := hookMap["command"].(string); ok && cmd == "tanagram" {
|
|
188
|
+
stopHookExists = true
|
|
189
|
+
break
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if beforeSubmitHookExists && stopHookExists {
|
|
194
|
+
fmt.Println("✓ Tanagram hooks are already configured in ~/.cursor/hooks.json")
|
|
195
|
+
return nil
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if !beforeSubmitHookExists {
|
|
199
|
+
beforeSubmitHook := map[string]interface{}{
|
|
200
|
+
"command": "tanagram snapshot",
|
|
201
|
+
}
|
|
202
|
+
beforeSubmitPrompt = append(beforeSubmitPrompt, beforeSubmitHook)
|
|
203
|
+
hooks["beforeSubmitPrompt"] = beforeSubmitPrompt
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if !stopHookExists {
|
|
207
|
+
stopHook := map[string]interface{}{
|
|
208
|
+
"command": "tanagram",
|
|
209
|
+
}
|
|
210
|
+
stopHooks = append(stopHooks, stopHook)
|
|
211
|
+
hooks["stop"] = stopHooks
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if err := saveConfig(hooksPath, hooksConfig); err != nil {
|
|
215
|
+
return fmt.Errorf("failed to save hooks: %w", err)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
fmt.Println("✓ Tanagram hooks added to ~/.cursor/hooks.json")
|
|
219
|
+
fmt.Println("\nCursor will now:")
|
|
220
|
+
fmt.Println(" - Snapshot file state before each prompt (beforeSubmitPrompt)")
|
|
221
|
+
fmt.Println(" - Check only Cursor's changes after agent completes (stop)")
|
|
222
|
+
fmt.Println(" - Send policy violations to Cursor for automatic fixing")
|
|
223
|
+
fmt.Println("\nThis prevents false positives from user-written code!")
|
|
224
|
+
|
|
225
|
+
return nil
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ConfigList shows where Tanagram hooks are installed
|
|
229
|
+
func ConfigList() error {
|
|
174
230
|
// Check user settings (~/.claude/settings.json)
|
|
175
|
-
userSettingsPath :=
|
|
176
|
-
|
|
231
|
+
userSettingsPath, err := GetHomeConfigPath(".claude", "settings.json")
|
|
232
|
+
if err != nil {
|
|
233
|
+
return err
|
|
234
|
+
}
|
|
235
|
+
userStatus := checkClaudeHookStatus(userSettingsPath)
|
|
177
236
|
|
|
178
237
|
// Check project settings (./.claude/settings.json)
|
|
179
238
|
projectSettingsPath := ".claude/settings.json"
|
|
180
|
-
projectStatus :=
|
|
239
|
+
projectStatus := checkClaudeHookStatus(projectSettingsPath)
|
|
240
|
+
|
|
241
|
+
// Check Cursor hooks (~/.cursor/hooks.json)
|
|
242
|
+
cursorHooksPath, err := GetHomeConfigPath(".cursor", "hooks.json")
|
|
243
|
+
if err != nil {
|
|
244
|
+
return err
|
|
245
|
+
}
|
|
246
|
+
cursorStatus := checkCursorHookStatus(cursorHooksPath)
|
|
181
247
|
|
|
182
248
|
// Print results
|
|
183
249
|
fmt.Println("Claude Code Hook Status:")
|
|
184
250
|
|
|
185
251
|
fmt.Printf("User Settings (~/.claude/settings.json):\n")
|
|
186
|
-
|
|
252
|
+
printClaudeHookStatus(userStatus, userSettingsPath)
|
|
187
253
|
|
|
188
254
|
fmt.Printf("\nProject Settings (./.claude/settings.json):\n")
|
|
189
|
-
|
|
255
|
+
printClaudeHookStatus(projectStatus, projectSettingsPath)
|
|
256
|
+
|
|
257
|
+
fmt.Println("\nCursor Hook Status:")
|
|
258
|
+
|
|
259
|
+
fmt.Printf("User Settings (~/.cursor/hooks.json):\n")
|
|
260
|
+
printCursorHookStatus(cursorStatus, cursorHooksPath)
|
|
190
261
|
|
|
191
262
|
return nil
|
|
192
263
|
}
|
|
193
264
|
|
|
194
|
-
//
|
|
195
|
-
type
|
|
265
|
+
// ClaudeHookStatus represents the status of a hook configuration
|
|
266
|
+
type ClaudeHookStatus struct {
|
|
196
267
|
FileExists bool
|
|
197
268
|
PreHookExists bool
|
|
198
269
|
PostHookExists bool
|
|
@@ -202,9 +273,20 @@ type HookStatus struct {
|
|
|
202
273
|
Error error
|
|
203
274
|
}
|
|
204
275
|
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
276
|
+
// CursorHookStatus represents the status of a Cursor hook configuration
|
|
277
|
+
type CursorHookStatus struct {
|
|
278
|
+
FileExists bool
|
|
279
|
+
BeforeSubmitExists bool
|
|
280
|
+
StopExists bool
|
|
281
|
+
IsUpToDate bool
|
|
282
|
+
BeforeSubmitCommand string
|
|
283
|
+
StopCommand string
|
|
284
|
+
Error error
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// checkClaudeHookStatus checks the status of Tanagram hook in a settings file
|
|
288
|
+
func checkClaudeHookStatus(settingsPath string) ClaudeHookStatus {
|
|
289
|
+
status := ClaudeHookStatus{
|
|
208
290
|
FileExists: false,
|
|
209
291
|
PreHookExists: false,
|
|
210
292
|
PostHookExists: false,
|
|
@@ -220,18 +302,12 @@ func checkHookStatus(settingsPath string) HookStatus {
|
|
|
220
302
|
|
|
221
303
|
status.FileExists = true
|
|
222
304
|
|
|
223
|
-
|
|
305
|
+
settings, err := loadConfig(settingsPath)
|
|
224
306
|
if err != nil {
|
|
225
307
|
status.Error = err
|
|
226
308
|
return status
|
|
227
309
|
}
|
|
228
310
|
|
|
229
|
-
var settings map[string]interface{}
|
|
230
|
-
if err := json.Unmarshal(data, &settings); err != nil {
|
|
231
|
-
status.Error = fmt.Errorf("invalid JSON: %w", err)
|
|
232
|
-
return status
|
|
233
|
-
}
|
|
234
|
-
|
|
235
311
|
hooks, ok := settings["hooks"].(map[string]interface{})
|
|
236
312
|
if !ok {
|
|
237
313
|
return status
|
|
@@ -310,8 +386,74 @@ func checkHookStatus(settingsPath string) HookStatus {
|
|
|
310
386
|
return status
|
|
311
387
|
}
|
|
312
388
|
|
|
313
|
-
//
|
|
314
|
-
func
|
|
389
|
+
// checkCursorHookStatus checks the status of Tanagram hooks in a Cursor hooks.json file
|
|
390
|
+
func checkCursorHookStatus(hooksPath string) CursorHookStatus {
|
|
391
|
+
status := CursorHookStatus{
|
|
392
|
+
FileExists: false,
|
|
393
|
+
BeforeSubmitExists: false,
|
|
394
|
+
StopExists: false,
|
|
395
|
+
IsUpToDate: false,
|
|
396
|
+
BeforeSubmitCommand: "",
|
|
397
|
+
StopCommand: "",
|
|
398
|
+
Error: nil,
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if _, err := os.Stat(hooksPath); os.IsNotExist(err) {
|
|
402
|
+
return status
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
status.FileExists = true
|
|
406
|
+
|
|
407
|
+
hooksConfig, err := loadConfig(hooksPath)
|
|
408
|
+
if err != nil {
|
|
409
|
+
status.Error = err
|
|
410
|
+
return status
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
hooks, ok := hooksConfig["hooks"].(map[string]interface{})
|
|
414
|
+
if !ok {
|
|
415
|
+
return status
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if beforeSubmitPrompt, ok := hooks["beforeSubmitPrompt"].([]interface{}); ok {
|
|
419
|
+
for _, hook := range beforeSubmitPrompt {
|
|
420
|
+
hookMap, ok := hook.(map[string]interface{})
|
|
421
|
+
if !ok {
|
|
422
|
+
continue
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
cmd, cmdOk := hookMap["command"].(string)
|
|
426
|
+
if cmdOk && cmd == "tanagram snapshot" {
|
|
427
|
+
status.BeforeSubmitExists = true
|
|
428
|
+
status.BeforeSubmitCommand = cmd
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if stopHooks, ok := hooks["stop"].([]interface{}); ok {
|
|
434
|
+
for _, hook := range stopHooks {
|
|
435
|
+
hookMap, ok := hook.(map[string]interface{})
|
|
436
|
+
if !ok {
|
|
437
|
+
continue
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
cmd, cmdOk := hookMap["command"].(string)
|
|
441
|
+
if cmdOk && cmd == "tanagram" {
|
|
442
|
+
status.StopExists = true
|
|
443
|
+
status.StopCommand = cmd
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if status.BeforeSubmitExists && status.StopExists {
|
|
449
|
+
status.IsUpToDate = true
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return status
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// printClaudeHookStatus prints the status of a hook in a human-readable format
|
|
456
|
+
func printClaudeHookStatus(status ClaudeHookStatus, path string) {
|
|
315
457
|
if status.Error != nil {
|
|
316
458
|
fmt.Printf(" ✗ Error: %v\n", status.Error)
|
|
317
459
|
return
|
|
@@ -350,18 +492,57 @@ func printHookStatus(status HookStatus, path string) {
|
|
|
350
492
|
}
|
|
351
493
|
}
|
|
352
494
|
|
|
353
|
-
//
|
|
495
|
+
// printCursorHookStatus prints the status of a Cursor hook in a human-readable format
|
|
496
|
+
func printCursorHookStatus(status CursorHookStatus, path string) {
|
|
497
|
+
if status.Error != nil {
|
|
498
|
+
fmt.Printf(" ✗ Error: %v\n", status.Error)
|
|
499
|
+
return
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if !status.FileExists {
|
|
503
|
+
fmt.Printf(" ○ Not configured (file does not exist)\n")
|
|
504
|
+
fmt.Printf(" → Run: tanagram config cursor\n")
|
|
505
|
+
return
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if !status.BeforeSubmitExists && !status.StopExists {
|
|
509
|
+
fmt.Printf(" ○ Not configured\n")
|
|
510
|
+
fmt.Printf(" → Run: tanagram config cursor\n")
|
|
511
|
+
return
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if status.IsUpToDate {
|
|
515
|
+
fmt.Printf(" ✓ Configured and up to date\n")
|
|
516
|
+
fmt.Printf(" beforeSubmitPrompt: %s\n", status.BeforeSubmitCommand)
|
|
517
|
+
fmt.Printf(" stop: %s\n", status.StopCommand)
|
|
518
|
+
fmt.Printf(" Location: %s\n", path)
|
|
519
|
+
} else {
|
|
520
|
+
fmt.Printf(" ⚠ Configured but incomplete\n")
|
|
521
|
+
if status.BeforeSubmitExists {
|
|
522
|
+
fmt.Printf(" ✓ beforeSubmitPrompt: %s\n", status.BeforeSubmitCommand)
|
|
523
|
+
} else {
|
|
524
|
+
fmt.Printf(" ✗ beforeSubmitPrompt: missing\n")
|
|
525
|
+
}
|
|
526
|
+
if status.StopExists {
|
|
527
|
+
fmt.Printf(" ✓ stop: %s\n", status.StopCommand)
|
|
528
|
+
} else {
|
|
529
|
+
fmt.Printf(" ✗ stop: missing\n")
|
|
530
|
+
}
|
|
531
|
+
fmt.Printf(" → Run: tanagram config cursor\n")
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// ensureClaudeConfigured checks if Claude Code hooks are configured,
|
|
354
536
|
// and automatically sets them up if not. This is called on first run
|
|
355
537
|
// of commands that need Claude Code integration.
|
|
356
|
-
func
|
|
357
|
-
|
|
538
|
+
func ensureClaudeConfigured() error {
|
|
539
|
+
settingsPath, err := GetHomeConfigPath(".claude", "settings.json")
|
|
358
540
|
if err != nil {
|
|
359
541
|
// Silently skip if we can't get home dir - not critical
|
|
360
542
|
return nil
|
|
361
543
|
}
|
|
362
544
|
|
|
363
|
-
|
|
364
|
-
status := checkHookStatus(settingsPath)
|
|
545
|
+
status := checkClaudeHookStatus(settingsPath)
|
|
365
546
|
|
|
366
547
|
// If hooks are already configured, we're done
|
|
367
548
|
if status.IsUpToDate {
|
|
@@ -371,7 +552,7 @@ func EnsureClaudeConfigured() error {
|
|
|
371
552
|
// If file doesn't exist or hooks aren't configured, set them up
|
|
372
553
|
if !status.FileExists || !status.PreHookExists || !status.PostHookExists {
|
|
373
554
|
fmt.Println("Setting up Claude Code integration...")
|
|
374
|
-
if err := ConfigClaude(); err != nil {
|
|
555
|
+
if err := ConfigClaude(settingsPath); err != nil {
|
|
375
556
|
// Don't fail the command if hook setup fails - just warn
|
|
376
557
|
fmt.Fprintf(os.Stderr, "Warning: Failed to setup Claude Code hooks: %v\n", err)
|
|
377
558
|
fmt.Fprintf(os.Stderr, "You can manually setup hooks later with: tanagram config claude\n\n")
|
|
@@ -382,3 +563,95 @@ func EnsureClaudeConfigured() error {
|
|
|
382
563
|
|
|
383
564
|
return nil
|
|
384
565
|
}
|
|
566
|
+
|
|
567
|
+
// ensureCursorConfigured checks if Cursor hooks are configured,
|
|
568
|
+
// and automatically sets them up if not. This is called on first run
|
|
569
|
+
// of commands that need Cursor integration.
|
|
570
|
+
func ensureCursorConfigured() error {
|
|
571
|
+
hooksPath, err := GetHomeConfigPath(".cursor", "hooks.json")
|
|
572
|
+
if err != nil {
|
|
573
|
+
// Silently skip if we can't get home dir - not critical
|
|
574
|
+
return nil
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
status := checkCursorHookStatus(hooksPath)
|
|
578
|
+
|
|
579
|
+
// If hooks are already configured, we're done
|
|
580
|
+
if status.IsUpToDate {
|
|
581
|
+
return nil
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// If file doesn't exist or hooks aren't configured, set them up
|
|
585
|
+
if !status.FileExists || !status.BeforeSubmitExists || !status.StopExists {
|
|
586
|
+
fmt.Println("Setting up Cursor integration...")
|
|
587
|
+
if err := ConfigCursor(hooksPath); err != nil {
|
|
588
|
+
// Don't fail the command if hook setup fails - just warn
|
|
589
|
+
fmt.Fprintf(os.Stderr, "Warning: Failed to setup Cursor hooks: %v\n", err)
|
|
590
|
+
fmt.Fprintf(os.Stderr, "You can manually setup hooks later with: tanagram config cursor\n\n")
|
|
591
|
+
return nil
|
|
592
|
+
}
|
|
593
|
+
fmt.Println() // Add blank line after setup message
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return nil
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// EnsureHooksConfigured ensures that both Claude Code and Cursor hooks are configured.
|
|
600
|
+
// This is a convenience function for commands that need both integrations.
|
|
601
|
+
func EnsureHooksConfigured() error {
|
|
602
|
+
if err := ensureClaudeConfigured(); err != nil {
|
|
603
|
+
return err
|
|
604
|
+
}
|
|
605
|
+
if err := ensureCursorConfigured(); err != nil {
|
|
606
|
+
return err
|
|
607
|
+
}
|
|
608
|
+
return nil
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Helpers
|
|
612
|
+
|
|
613
|
+
func GetHomeConfigPath(parts ...string) (string, error) {
|
|
614
|
+
home, err := os.UserHomeDir()
|
|
615
|
+
if err != nil {
|
|
616
|
+
return "", fmt.Errorf("failed to get home directory: %w", err)
|
|
617
|
+
}
|
|
618
|
+
// Prepend home to parts
|
|
619
|
+
allParts := append([]string{home}, parts...)
|
|
620
|
+
return filepath.Join(allParts...), nil
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
func loadConfig(path string) (map[string]interface{}, error) {
|
|
624
|
+
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
625
|
+
return make(map[string]interface{}), nil
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
data, err := os.ReadFile(path)
|
|
629
|
+
if err != nil {
|
|
630
|
+
return nil, fmt.Errorf("failed to read config: %w", err)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
var config map[string]interface{}
|
|
634
|
+
if err := json.Unmarshal(data, &config); err != nil {
|
|
635
|
+
return nil, fmt.Errorf("failed to parse config (invalid JSON): %w", err)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return config, nil
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
func saveConfig(path string, config map[string]interface{}) error {
|
|
642
|
+
dir := filepath.Dir(path)
|
|
643
|
+
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
644
|
+
return fmt.Errorf("failed to create directory: %w", err)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
data, err := json.MarshalIndent(config, "", " ")
|
|
648
|
+
if err != nil {
|
|
649
|
+
return fmt.Errorf("failed to marshal config: %w", err)
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if err := os.WriteFile(path, data, 0644); err != nil {
|
|
653
|
+
return fmt.Errorf("failed to write config: %w", err)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return nil
|
|
657
|
+
}
|