@tanagram/cli 0.4.1 → 0.4.3
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/commands/config.go +343 -70
- package/commands/config_test.go +389 -0
- package/main.go +32 -14
- package/package.json +1 -1
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
|
+
}
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
package commands
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"testing"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func TestConfigClaude(t *testing.T) {
|
|
11
|
+
// Create a temporary directory
|
|
12
|
+
tempDir, err := os.MkdirTemp("", "tanagram-config-test-*")
|
|
13
|
+
if err != nil {
|
|
14
|
+
t.Fatalf("Failed to create temp dir: %v", err)
|
|
15
|
+
}
|
|
16
|
+
defer os.RemoveAll(tempDir)
|
|
17
|
+
|
|
18
|
+
settingsPath := filepath.Join(tempDir, ".claude", "settings.json")
|
|
19
|
+
|
|
20
|
+
// Redirect stdout to suppress logs
|
|
21
|
+
oldStdout := os.Stdout
|
|
22
|
+
_, w, _ := os.Pipe()
|
|
23
|
+
os.Stdout = w
|
|
24
|
+
defer func() {
|
|
25
|
+
os.Stdout = oldStdout
|
|
26
|
+
}()
|
|
27
|
+
|
|
28
|
+
t.Run("Create new config", func(t *testing.T) {
|
|
29
|
+
if err := ConfigClaude(settingsPath); err != nil {
|
|
30
|
+
t.Fatalf("ConfigClaude() failed: %v", err)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
settings := readSettings(t, settingsPath)
|
|
34
|
+
pre, post := checkHooks(settings)
|
|
35
|
+
if !pre {
|
|
36
|
+
t.Error("PreToolUse hook not created")
|
|
37
|
+
}
|
|
38
|
+
if !post {
|
|
39
|
+
t.Error("PostToolUse hook not created")
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
t.Run("Idempotency", func(t *testing.T) {
|
|
44
|
+
// Run it again (first time was in "Create new config")
|
|
45
|
+
if err := ConfigClaude(settingsPath); err != nil {
|
|
46
|
+
t.Fatalf("ConfigClaude() failed on second run: %v", err)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
settings := readSettings(t, settingsPath)
|
|
50
|
+
pre, post := checkHooks(settings)
|
|
51
|
+
if !pre || !post {
|
|
52
|
+
t.Error("Hooks missing after second run")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check that we didn't duplicate them (rudimentary check: count of items)
|
|
56
|
+
hooks := settings["hooks"].(map[string]interface{})
|
|
57
|
+
preToolUse := hooks["PreToolUse"].([]interface{})
|
|
58
|
+
postToolUse := hooks["PostToolUse"].([]interface{})
|
|
59
|
+
|
|
60
|
+
// We expect exactly 1 hook in each since we started fresh
|
|
61
|
+
if len(preToolUse) != 1 {
|
|
62
|
+
t.Errorf("Expected 1 PreToolUse hook, got %d", len(preToolUse))
|
|
63
|
+
}
|
|
64
|
+
if len(postToolUse) != 1 {
|
|
65
|
+
t.Errorf("Expected 1 PostToolUse hook, got %d", len(postToolUse))
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
t.Run("Preserve existing hooks", func(t *testing.T) {
|
|
70
|
+
// Reset with a file that has other hooks
|
|
71
|
+
otherHook := map[string]interface{}{
|
|
72
|
+
"hooks": map[string]interface{}{
|
|
73
|
+
"PreToolUse": []interface{}{
|
|
74
|
+
map[string]interface{}{
|
|
75
|
+
"matcher": "Read",
|
|
76
|
+
"hooks": []interface{}{
|
|
77
|
+
map[string]interface{}{"command": "echo existing"},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Clean up previous run
|
|
85
|
+
os.RemoveAll(filepath.Dir(settingsPath))
|
|
86
|
+
|
|
87
|
+
dir := filepath.Dir(settingsPath)
|
|
88
|
+
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
89
|
+
t.Fatalf("Failed to create dir: %v", err)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Write otherHook to settingsPath
|
|
93
|
+
data, _ := json.Marshal(otherHook)
|
|
94
|
+
if err := os.WriteFile(settingsPath, data, 0644); err != nil {
|
|
95
|
+
t.Fatalf("Failed to write settings: %v", err)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if err := ConfigClaude(settingsPath); err != nil {
|
|
99
|
+
t.Fatalf("ConfigClaude() failed: %v", err)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
settings := readSettings(t, settingsPath)
|
|
103
|
+
pre, post := checkHooks(settings)
|
|
104
|
+
if !pre || !post {
|
|
105
|
+
t.Error("Tanagram hooks missing when merging with existing config")
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check if existing hook is still there
|
|
109
|
+
hooks := settings["hooks"].(map[string]interface{})
|
|
110
|
+
preToolUse := hooks["PreToolUse"].([]interface{})
|
|
111
|
+
|
|
112
|
+
foundExisting := false
|
|
113
|
+
for _, h := range preToolUse {
|
|
114
|
+
hMap := h.(map[string]interface{})
|
|
115
|
+
if hMap["matcher"] == "Read" {
|
|
116
|
+
// Check for the specific command to be sure it's the right one
|
|
117
|
+
if commands, ok := hMap["hooks"].([]interface{}); ok {
|
|
118
|
+
for _, cmd := range commands {
|
|
119
|
+
if cmdMap, ok := cmd.(map[string]interface{}); ok {
|
|
120
|
+
if cmdMap["command"] == "echo existing" {
|
|
121
|
+
foundExisting = true
|
|
122
|
+
break
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if foundExisting {
|
|
129
|
+
break
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if !foundExisting {
|
|
133
|
+
t.Error("Existing hook was lost")
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
func TestConfigCursor(t *testing.T) {
|
|
139
|
+
// Create a temporary directory
|
|
140
|
+
tempDir, err := os.MkdirTemp("", "tanagram-cursor-test-*")
|
|
141
|
+
if err != nil {
|
|
142
|
+
t.Fatalf("Failed to create temp dir: %v", err)
|
|
143
|
+
}
|
|
144
|
+
defer os.RemoveAll(tempDir)
|
|
145
|
+
|
|
146
|
+
hooksPath := filepath.Join(tempDir, ".cursor", "hooks.json")
|
|
147
|
+
|
|
148
|
+
// Redirect stdout to suppress logs
|
|
149
|
+
oldStdout := os.Stdout
|
|
150
|
+
_, w, _ := os.Pipe()
|
|
151
|
+
os.Stdout = w
|
|
152
|
+
defer func() {
|
|
153
|
+
os.Stdout = oldStdout
|
|
154
|
+
}()
|
|
155
|
+
|
|
156
|
+
t.Run("Create new config", func(t *testing.T) {
|
|
157
|
+
if err := ConfigCursor(hooksPath); err != nil {
|
|
158
|
+
t.Fatalf("ConfigCursor() failed: %v", err)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
settings := readSettings(t, hooksPath)
|
|
162
|
+
beforeSubmit, stop := checkCursorHooks(settings)
|
|
163
|
+
if !beforeSubmit {
|
|
164
|
+
t.Error("beforeSubmitPrompt hook not created")
|
|
165
|
+
}
|
|
166
|
+
if !stop {
|
|
167
|
+
t.Error("stop hook not created")
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
t.Run("Idempotency", func(t *testing.T) {
|
|
172
|
+
// Run it again
|
|
173
|
+
if err := ConfigCursor(hooksPath); err != nil {
|
|
174
|
+
t.Fatalf("ConfigCursor() failed on second run: %v", err)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
settings := readSettings(t, hooksPath)
|
|
178
|
+
beforeSubmit, stop := checkCursorHooks(settings)
|
|
179
|
+
if !beforeSubmit || !stop {
|
|
180
|
+
t.Error("Hooks missing after second run")
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Check duplicates
|
|
184
|
+
hooks := settings["hooks"].(map[string]interface{})
|
|
185
|
+
beforeSubmitHooks := hooks["beforeSubmitPrompt"].([]interface{})
|
|
186
|
+
stopHooks := hooks["stop"].([]interface{})
|
|
187
|
+
|
|
188
|
+
if len(beforeSubmitHooks) != 1 {
|
|
189
|
+
t.Errorf("Expected 1 beforeSubmitPrompt hook, got %d", len(beforeSubmitHooks))
|
|
190
|
+
}
|
|
191
|
+
if len(stopHooks) != 1 {
|
|
192
|
+
t.Errorf("Expected 1 stop hook, got %d", len(stopHooks))
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
t.Run("Preserve existing hooks", func(t *testing.T) {
|
|
197
|
+
// Reset with a file that has other hooks
|
|
198
|
+
otherHook := map[string]interface{}{
|
|
199
|
+
"hooks": map[string]interface{}{
|
|
200
|
+
"beforeSubmitPrompt": []interface{}{
|
|
201
|
+
map[string]interface{}{"command": "echo existing"},
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Clean up previous run
|
|
207
|
+
os.RemoveAll(filepath.Dir(hooksPath))
|
|
208
|
+
|
|
209
|
+
dir := filepath.Dir(hooksPath)
|
|
210
|
+
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
211
|
+
t.Fatalf("Failed to create dir: %v", err)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Write otherHook to hooksPath
|
|
215
|
+
data, _ := json.Marshal(otherHook)
|
|
216
|
+
if err := os.WriteFile(hooksPath, data, 0644); err != nil {
|
|
217
|
+
t.Fatalf("Failed to write settings: %v", err)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if err := ConfigCursor(hooksPath); err != nil {
|
|
221
|
+
t.Fatalf("ConfigCursor() failed: %v", err)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
settings := readSettings(t, hooksPath)
|
|
225
|
+
beforeSubmit, stop := checkCursorHooks(settings)
|
|
226
|
+
if !beforeSubmit || !stop {
|
|
227
|
+
t.Error("Tanagram hooks missing when merging with existing config")
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check if existing hook is still there
|
|
231
|
+
hooks := settings["hooks"].(map[string]interface{})
|
|
232
|
+
beforeSubmitHooks := hooks["beforeSubmitPrompt"].([]interface{})
|
|
233
|
+
|
|
234
|
+
foundExisting := false
|
|
235
|
+
for _, h := range beforeSubmitHooks {
|
|
236
|
+
hMap := h.(map[string]interface{})
|
|
237
|
+
if cmd, ok := hMap["command"].(string); ok && cmd == "echo existing" {
|
|
238
|
+
foundExisting = true
|
|
239
|
+
break
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if !foundExisting {
|
|
243
|
+
t.Error("Existing hook was lost")
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
func TestEnsureHooksConfigured(t *testing.T) {
|
|
249
|
+
// Create a temporary home directory
|
|
250
|
+
tempHome, err := os.MkdirTemp("", "tanagram-home-test-*")
|
|
251
|
+
if err != nil {
|
|
252
|
+
t.Fatalf("Failed to create temp home: %v", err)
|
|
253
|
+
}
|
|
254
|
+
defer os.RemoveAll(tempHome)
|
|
255
|
+
|
|
256
|
+
// Mock HOME environment variable
|
|
257
|
+
originalHome := os.Getenv("HOME")
|
|
258
|
+
defer os.Setenv("HOME", originalHome)
|
|
259
|
+
os.Setenv("HOME", tempHome)
|
|
260
|
+
|
|
261
|
+
// Redirect stdout/stderr to suppress logs
|
|
262
|
+
oldStdout := os.Stdout
|
|
263
|
+
oldStderr := os.Stderr
|
|
264
|
+
_, w, _ := os.Pipe()
|
|
265
|
+
os.Stdout = w
|
|
266
|
+
os.Stderr = w
|
|
267
|
+
defer func() {
|
|
268
|
+
os.Stdout = oldStdout
|
|
269
|
+
os.Stderr = oldStderr
|
|
270
|
+
}()
|
|
271
|
+
|
|
272
|
+
t.Run("Fresh configuration", func(t *testing.T) {
|
|
273
|
+
if err := EnsureHooksConfigured(); err != nil {
|
|
274
|
+
t.Fatalf("EnsureHooksConfigured() failed: %v", err)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Check Claude settings
|
|
278
|
+
claudeSettingsPath := filepath.Join(tempHome, ".claude", "settings.json")
|
|
279
|
+
claudeSettings := readSettings(t, claudeSettingsPath)
|
|
280
|
+
pre, post := checkHooks(claudeSettings)
|
|
281
|
+
if !pre {
|
|
282
|
+
t.Error("PreToolUse hook not created")
|
|
283
|
+
}
|
|
284
|
+
if !post {
|
|
285
|
+
t.Error("PostToolUse hook not created")
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Check Cursor hooks
|
|
289
|
+
cursorHooksPath := filepath.Join(tempHome, ".cursor", "hooks.json")
|
|
290
|
+
cursorSettings := readSettings(t, cursorHooksPath)
|
|
291
|
+
beforeSubmit, stop := checkCursorHooks(cursorSettings)
|
|
292
|
+
if !beforeSubmit {
|
|
293
|
+
t.Error("beforeSubmitPrompt hook not created")
|
|
294
|
+
}
|
|
295
|
+
if !stop {
|
|
296
|
+
t.Error("stop hook not created")
|
|
297
|
+
}
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
func readSettings(t *testing.T, settingsPath string) map[string]interface{} {
|
|
302
|
+
data, err := os.ReadFile(settingsPath)
|
|
303
|
+
if err != nil {
|
|
304
|
+
t.Fatalf("Failed to read settings file: %v", err)
|
|
305
|
+
}
|
|
306
|
+
var settings map[string]interface{}
|
|
307
|
+
if err := json.Unmarshal(data, &settings); err != nil {
|
|
308
|
+
t.Fatalf("Failed to parse settings JSON: %v", err)
|
|
309
|
+
}
|
|
310
|
+
return settings
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
func checkHooks(settings map[string]interface{}) (bool, bool) {
|
|
314
|
+
hooks, ok := settings["hooks"].(map[string]interface{})
|
|
315
|
+
if !ok {
|
|
316
|
+
return false, false
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
preExists := false
|
|
320
|
+
if preToolUse, ok := hooks["PreToolUse"].([]interface{}); ok {
|
|
321
|
+
for _, h := range preToolUse {
|
|
322
|
+
if hMap, ok := h.(map[string]interface{}); ok {
|
|
323
|
+
if hMap["matcher"] == "Edit|Write" {
|
|
324
|
+
if innerHooks, ok := hMap["hooks"].([]interface{}); ok {
|
|
325
|
+
for _, ih := range innerHooks {
|
|
326
|
+
if ihMap, ok := ih.(map[string]interface{}); ok {
|
|
327
|
+
if ihMap["command"] == "tanagram snapshot" {
|
|
328
|
+
preExists = true
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
postExists := false
|
|
339
|
+
if postToolUse, ok := hooks["PostToolUse"].([]interface{}); ok {
|
|
340
|
+
for _, h := range postToolUse {
|
|
341
|
+
if hMap, ok := h.(map[string]interface{}); ok {
|
|
342
|
+
if hMap["matcher"] == "Edit|Write" {
|
|
343
|
+
if innerHooks, ok := hMap["hooks"].([]interface{}); ok {
|
|
344
|
+
for _, ih := range innerHooks {
|
|
345
|
+
if ihMap, ok := ih.(map[string]interface{}); ok {
|
|
346
|
+
if ihMap["command"] == "tanagram" {
|
|
347
|
+
postExists = true
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return preExists, postExists
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
func checkCursorHooks(settings map[string]interface{}) (bool, bool) {
|
|
361
|
+
hooks, ok := settings["hooks"].(map[string]interface{})
|
|
362
|
+
if !ok {
|
|
363
|
+
return false, false
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
beforeSubmitExists := false
|
|
367
|
+
if beforeSubmit, ok := hooks["beforeSubmitPrompt"].([]interface{}); ok {
|
|
368
|
+
for _, h := range beforeSubmit {
|
|
369
|
+
if hMap, ok := h.(map[string]interface{}); ok {
|
|
370
|
+
if cmd, ok := hMap["command"].(string); ok && cmd == "tanagram snapshot" {
|
|
371
|
+
beforeSubmitExists = true
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
stopExists := false
|
|
378
|
+
if stop, ok := hooks["stop"].([]interface{}); ok {
|
|
379
|
+
for _, h := range stop {
|
|
380
|
+
if hMap, ok := h.(map[string]interface{}); ok {
|
|
381
|
+
if cmd, ok := hMap["command"].(string); ok && cmd == "tanagram" {
|
|
382
|
+
stopExists = true
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return beforeSubmitExists, stopExists
|
|
389
|
+
}
|
package/main.go
CHANGED
|
@@ -43,8 +43,8 @@ func main() {
|
|
|
43
43
|
metrics.Track("cli.command.execute", map[string]interface{}{
|
|
44
44
|
"command": "run",
|
|
45
45
|
})
|
|
46
|
-
// Auto-setup
|
|
47
|
-
if err := commands.
|
|
46
|
+
// Auto-setup hooks on first run
|
|
47
|
+
if err := commands.EnsureHooksConfigured(); err != nil {
|
|
48
48
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
49
49
|
exitCode = 1
|
|
50
50
|
return
|
|
@@ -59,8 +59,8 @@ func main() {
|
|
|
59
59
|
metrics.Track("cli.command.execute", map[string]interface{}{
|
|
60
60
|
"command": "sync",
|
|
61
61
|
})
|
|
62
|
-
// Auto-setup
|
|
63
|
-
if err := commands.
|
|
62
|
+
// Auto-setup hooks on first run
|
|
63
|
+
if err := commands.EnsureHooksConfigured(); err != nil {
|
|
64
64
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
65
65
|
exitCode = 1
|
|
66
66
|
return
|
|
@@ -70,8 +70,8 @@ func main() {
|
|
|
70
70
|
metrics.Track("cli.command.execute", map[string]interface{}{
|
|
71
71
|
"command": "list",
|
|
72
72
|
})
|
|
73
|
-
// Auto-setup
|
|
74
|
-
if err := commands.
|
|
73
|
+
// Auto-setup hooks on first run
|
|
74
|
+
if err := commands.EnsureHooksConfigured(); err != nil {
|
|
75
75
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
76
76
|
exitCode = 1
|
|
77
77
|
return
|
|
@@ -83,6 +83,7 @@ func main() {
|
|
|
83
83
|
fmt.Fprintf(os.Stderr, "Usage: tanagram config <subcommand>\n")
|
|
84
84
|
fmt.Fprintf(os.Stderr, "\nAvailable subcommands:\n")
|
|
85
85
|
fmt.Fprintf(os.Stderr, " claude Setup Claude Code hook\n")
|
|
86
|
+
fmt.Fprintf(os.Stderr, " cursor Setup Cursor hook\n")
|
|
86
87
|
fmt.Fprintf(os.Stderr, " list Show hook installation status\n")
|
|
87
88
|
exitCode = 1
|
|
88
89
|
return
|
|
@@ -93,7 +94,22 @@ func main() {
|
|
|
93
94
|
metrics.Track("cli.command.execute", map[string]interface{}{
|
|
94
95
|
"command": "config.claude",
|
|
95
96
|
})
|
|
96
|
-
|
|
97
|
+
settingsPath, pathErr := commands.GetHomeConfigPath(".claude", "settings.json")
|
|
98
|
+
if pathErr != nil {
|
|
99
|
+
err = pathErr
|
|
100
|
+
break
|
|
101
|
+
}
|
|
102
|
+
err = commands.ConfigClaude(settingsPath)
|
|
103
|
+
case "cursor":
|
|
104
|
+
metrics.Track("cli.command.execute", map[string]interface{}{
|
|
105
|
+
"command": "config.cursor",
|
|
106
|
+
})
|
|
107
|
+
hooksPath, pathErr := commands.GetHomeConfigPath(".cursor", "hooks.json")
|
|
108
|
+
if pathErr != nil {
|
|
109
|
+
err = pathErr
|
|
110
|
+
break
|
|
111
|
+
}
|
|
112
|
+
err = commands.ConfigCursor(hooksPath)
|
|
97
113
|
case "list":
|
|
98
114
|
metrics.Track("cli.command.execute", map[string]interface{}{
|
|
99
115
|
"command": "config.list",
|
|
@@ -179,14 +195,12 @@ USAGE:
|
|
|
179
195
|
COMMANDS:
|
|
180
196
|
run Check git changes against policies (default)
|
|
181
197
|
login Authenticate with Tanagram using Stytch B2B
|
|
182
|
-
<<<<<<< HEAD
|
|
183
198
|
sync-policies Sync cloud policies from Tanagram
|
|
184
|
-
=======
|
|
185
|
-
>>>>>>> origin/main
|
|
186
199
|
snapshot Create a snapshot of current file state (used by PreToolUse hook)
|
|
187
200
|
sync Manually sync instruction files to cache
|
|
188
201
|
list Show all cached policies
|
|
189
202
|
config claude Setup Claude Code hook automatically
|
|
203
|
+
config cursor Setup Cursor hook automatically
|
|
190
204
|
config list Show hook installation status
|
|
191
205
|
version Show the CLI version
|
|
192
206
|
help Show this help message
|
|
@@ -195,14 +209,12 @@ EXAMPLES:
|
|
|
195
209
|
tanagram # Check changes (auto-syncs if files changed)
|
|
196
210
|
tanagram run # Same as above
|
|
197
211
|
tanagram login # Authenticate with Tanagram
|
|
198
|
-
<<<<<<< HEAD
|
|
199
212
|
tanagram sync-policies # Sync cloud policies from Tanagram
|
|
200
|
-
=======
|
|
201
|
-
>>>>>>> origin/main
|
|
202
213
|
tanagram snapshot # Create snapshot before making changes
|
|
203
214
|
tanagram sync # Manually sync local instruction files
|
|
204
215
|
tanagram list # View all cached policies
|
|
205
216
|
tanagram config claude # Setup Claude Code hooks in ~/.claude/settings.json
|
|
217
|
+
tanagram config cursor # Setup Cursor hooks in ~/.cursor/hooks.json
|
|
206
218
|
tanagram config list # Show where hooks are installed
|
|
207
219
|
tanagram version # Show the version
|
|
208
220
|
|
|
@@ -214,9 +226,15 @@ INSTRUCTION FILES:
|
|
|
214
226
|
Policies are cached and automatically resynced when files change.
|
|
215
227
|
|
|
216
228
|
HOOK WORKFLOW:
|
|
217
|
-
When configured with 'tanagram config claude'
|
|
229
|
+
When configured with 'tanagram config claude' or 'tanagram config cursor':
|
|
230
|
+
|
|
231
|
+
Claude Code:
|
|
218
232
|
1. PreToolUse (Edit|Write): Creates snapshot before Claude makes changes
|
|
219
233
|
2. PostToolUse (Edit|Write): Checks only Claude's changes against policies
|
|
234
|
+
|
|
235
|
+
Cursor:
|
|
236
|
+
1. beforeSubmitPrompt: Creates snapshot before agent starts working
|
|
237
|
+
2. stop: Checks only Cursor's changes after agent completes
|
|
220
238
|
|
|
221
239
|
This prevents false positives from user-written code!
|
|
222
240
|
`
|