@tanagram/cli 0.1.27 → 0.1.28
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 +273 -0
- package/main.go +37 -8
- package/package.json +1 -1
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
package commands
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"strings"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// ConfigClaude sets up the Claude Code hook in ~/.claude/settings.json
|
|
12
|
+
func ConfigClaude() error {
|
|
13
|
+
// Get the Claude settings file path
|
|
14
|
+
home, err := os.UserHomeDir()
|
|
15
|
+
if err != nil {
|
|
16
|
+
return fmt.Errorf("failed to get home directory: %w", err)
|
|
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
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check if hooks already exist
|
|
42
|
+
hooks, hooksExist := settings["hooks"].(map[string]interface{})
|
|
43
|
+
if !hooksExist {
|
|
44
|
+
hooks = make(map[string]interface{})
|
|
45
|
+
settings["hooks"] = hooks
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check if PostToolUse exists
|
|
49
|
+
postToolUse, postToolUseExist := hooks["PostToolUse"].([]interface{})
|
|
50
|
+
if !postToolUseExist {
|
|
51
|
+
postToolUse = []interface{}{}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check if tanagram hook already exists
|
|
55
|
+
tanaramExists := false
|
|
56
|
+
for _, hook := range postToolUse {
|
|
57
|
+
hookMap, ok := hook.(map[string]interface{})
|
|
58
|
+
if !ok {
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check if this is the Edit|Write matcher
|
|
63
|
+
if matcher, ok := hookMap["matcher"].(string); ok && matcher == "Edit|Write" {
|
|
64
|
+
// Check if tanagram is in the hooks array
|
|
65
|
+
innerHooks, ok := hookMap["hooks"].([]interface{})
|
|
66
|
+
if ok {
|
|
67
|
+
for _, innerHook := range innerHooks {
|
|
68
|
+
innerHookMap, ok := innerHook.(map[string]interface{})
|
|
69
|
+
if ok {
|
|
70
|
+
if cmd, ok := innerHookMap["command"].(string); ok && cmd == "tanagram" {
|
|
71
|
+
tanaramExists = true
|
|
72
|
+
break
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if tanaramExists {
|
|
81
|
+
fmt.Println("✓ Tanagram hook is already configured in ~/.claude/settings.json")
|
|
82
|
+
return nil
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Add tanagram hook
|
|
86
|
+
tanaramHook := map[string]interface{}{
|
|
87
|
+
"matcher": "Edit|Write",
|
|
88
|
+
"hooks": []interface{}{
|
|
89
|
+
map[string]interface{}{
|
|
90
|
+
"type": "command",
|
|
91
|
+
"command": "tanagram",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
postToolUse = append(postToolUse, tanaramHook)
|
|
97
|
+
hooks["PostToolUse"] = postToolUse
|
|
98
|
+
|
|
99
|
+
// Ensure .claude directory exists
|
|
100
|
+
claudeDir := filepath.Join(home, ".claude")
|
|
101
|
+
if err := os.MkdirAll(claudeDir, 0755); err != nil {
|
|
102
|
+
return fmt.Errorf("failed to create .claude directory: %w", err)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Write updated settings back to file
|
|
106
|
+
data, err := json.MarshalIndent(settings, "", " ")
|
|
107
|
+
if err != nil {
|
|
108
|
+
return fmt.Errorf("failed to marshal settings: %w", err)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if err := os.WriteFile(settingsPath, data, 0644); err != nil {
|
|
112
|
+
return fmt.Errorf("failed to write settings: %w", err)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fmt.Println("✓ Tanagram hook added to ~/.claude/settings.json")
|
|
116
|
+
fmt.Println("\nClaude Code will now automatically run Tanagram after Edit/Write operations.")
|
|
117
|
+
fmt.Println("Any policy violations will be sent to Claude for automatic fixing.")
|
|
118
|
+
|
|
119
|
+
return nil
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ConfigList shows where Tanagram hooks are installed
|
|
123
|
+
func ConfigList() error {
|
|
124
|
+
home, err := os.UserHomeDir()
|
|
125
|
+
if err != nil {
|
|
126
|
+
return fmt.Errorf("failed to get home directory: %w", err)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check user settings (~/.claude/settings.json)
|
|
130
|
+
userSettingsPath := filepath.Join(home, ".claude", "settings.json")
|
|
131
|
+
userStatus := checkHookStatus(userSettingsPath)
|
|
132
|
+
|
|
133
|
+
// Check project settings (./.claude/settings.json)
|
|
134
|
+
projectSettingsPath := ".claude/settings.json"
|
|
135
|
+
projectStatus := checkHookStatus(projectSettingsPath)
|
|
136
|
+
|
|
137
|
+
// Print results
|
|
138
|
+
fmt.Println("Tanagram Hook Status:\n")
|
|
139
|
+
|
|
140
|
+
fmt.Printf("User Settings (~/.claude/settings.json):\n")
|
|
141
|
+
printHookStatus(userStatus, userSettingsPath)
|
|
142
|
+
|
|
143
|
+
fmt.Printf("\nProject Settings (./.claude/settings.json):\n")
|
|
144
|
+
printHookStatus(projectStatus, projectSettingsPath)
|
|
145
|
+
|
|
146
|
+
return nil
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// HookStatus represents the status of a hook configuration
|
|
150
|
+
type HookStatus struct {
|
|
151
|
+
FileExists bool
|
|
152
|
+
HookExists bool
|
|
153
|
+
IsUpToDate bool
|
|
154
|
+
Command string
|
|
155
|
+
Error error
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// checkHookStatus checks the status of Tanagram hook in a settings file
|
|
159
|
+
func checkHookStatus(settingsPath string) HookStatus {
|
|
160
|
+
status := HookStatus{
|
|
161
|
+
FileExists: false,
|
|
162
|
+
HookExists: false,
|
|
163
|
+
IsUpToDate: false,
|
|
164
|
+
Command: "",
|
|
165
|
+
Error: nil,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Check if file exists
|
|
169
|
+
if _, err := os.Stat(settingsPath); os.IsNotExist(err) {
|
|
170
|
+
return status
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
status.FileExists = true
|
|
174
|
+
|
|
175
|
+
// Read file
|
|
176
|
+
data, err := os.ReadFile(settingsPath)
|
|
177
|
+
if err != nil {
|
|
178
|
+
status.Error = err
|
|
179
|
+
return status
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Parse JSON
|
|
183
|
+
var settings map[string]interface{}
|
|
184
|
+
if err := json.Unmarshal(data, &settings); err != nil {
|
|
185
|
+
status.Error = fmt.Errorf("invalid JSON: %w", err)
|
|
186
|
+
return status
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check for hooks
|
|
190
|
+
hooks, ok := settings["hooks"].(map[string]interface{})
|
|
191
|
+
if !ok {
|
|
192
|
+
return status
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
postToolUse, ok := hooks["PostToolUse"].([]interface{})
|
|
196
|
+
if !ok {
|
|
197
|
+
return status
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Look for tanagram hook
|
|
201
|
+
for _, hook := range postToolUse {
|
|
202
|
+
hookMap, ok := hook.(map[string]interface{})
|
|
203
|
+
if !ok {
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Check if this is the Edit|Write matcher
|
|
208
|
+
matcher, ok := hookMap["matcher"].(string)
|
|
209
|
+
if !ok {
|
|
210
|
+
continue
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check if tanagram is in the hooks array
|
|
214
|
+
innerHooks, ok := hookMap["hooks"].([]interface{})
|
|
215
|
+
if !ok {
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for _, innerHook := range innerHooks {
|
|
220
|
+
innerHookMap, ok := innerHook.(map[string]interface{})
|
|
221
|
+
if !ok {
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
cmd, cmdOk := innerHookMap["command"].(string)
|
|
226
|
+
hookType, typeOk := innerHookMap["type"].(string)
|
|
227
|
+
|
|
228
|
+
if cmdOk && strings.Contains(cmd, "tanagram") {
|
|
229
|
+
status.HookExists = true
|
|
230
|
+
status.Command = cmd
|
|
231
|
+
|
|
232
|
+
// Check if it's up to date (should be "tanagram" and type "command" and matcher "Edit|Write")
|
|
233
|
+
if cmd == "tanagram" && typeOk && hookType == "command" && matcher == "Edit|Write" {
|
|
234
|
+
status.IsUpToDate = true
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return status
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// printHookStatus prints the status of a hook in a human-readable format
|
|
244
|
+
func printHookStatus(status HookStatus, path string) {
|
|
245
|
+
if status.Error != nil {
|
|
246
|
+
fmt.Printf(" ✗ Error: %v\n", status.Error)
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if !status.FileExists {
|
|
251
|
+
fmt.Printf(" ○ Not configured (file does not exist)\n")
|
|
252
|
+
fmt.Printf(" → Run: tanagram config claude\n")
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if !status.HookExists {
|
|
257
|
+
fmt.Printf(" ○ Not configured\n")
|
|
258
|
+
fmt.Printf(" → Run: tanagram config claude\n")
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if status.IsUpToDate {
|
|
263
|
+
fmt.Printf(" ✓ Configured and up to date\n")
|
|
264
|
+
fmt.Printf(" Command: %s\n", status.Command)
|
|
265
|
+
fmt.Printf(" Location: %s\n", path)
|
|
266
|
+
} else {
|
|
267
|
+
fmt.Printf(" ⚠ Configured but outdated\n")
|
|
268
|
+
fmt.Printf(" Current: %s\n", status.Command)
|
|
269
|
+
fmt.Printf(" Expected: tanagram\n")
|
|
270
|
+
fmt.Printf(" → Run: tanagram config claude\n")
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
package/main.go
CHANGED
|
@@ -37,6 +37,31 @@ func main() {
|
|
|
37
37
|
"command": "list",
|
|
38
38
|
})
|
|
39
39
|
err = commands.List()
|
|
40
|
+
case "config":
|
|
41
|
+
// Handle config subcommands
|
|
42
|
+
if len(os.Args) < 3 {
|
|
43
|
+
fmt.Fprintf(os.Stderr, "Usage: tanagram config <subcommand>\n")
|
|
44
|
+
fmt.Fprintf(os.Stderr, "\nAvailable subcommands:\n")
|
|
45
|
+
fmt.Fprintf(os.Stderr, " claude Setup Claude Code hook\n")
|
|
46
|
+
fmt.Fprintf(os.Stderr, " list Show hook installation status\n")
|
|
47
|
+
os.Exit(1)
|
|
48
|
+
}
|
|
49
|
+
subCmd := os.Args[2]
|
|
50
|
+
switch subCmd {
|
|
51
|
+
case "claude":
|
|
52
|
+
metrics.Track("command.execute", map[string]interface{}{
|
|
53
|
+
"command": "config.claude",
|
|
54
|
+
})
|
|
55
|
+
err = commands.ConfigClaude()
|
|
56
|
+
case "list":
|
|
57
|
+
metrics.Track("command.execute", map[string]interface{}{
|
|
58
|
+
"command": "config.list",
|
|
59
|
+
})
|
|
60
|
+
err = commands.ConfigList()
|
|
61
|
+
default:
|
|
62
|
+
fmt.Fprintf(os.Stderr, "Unknown config subcommand: %s\n", subCmd)
|
|
63
|
+
os.Exit(1)
|
|
64
|
+
}
|
|
40
65
|
case "welcome":
|
|
41
66
|
metrics.Track("command.execute", map[string]interface{}{
|
|
42
67
|
"command": "welcome",
|
|
@@ -93,16 +118,20 @@ USAGE:
|
|
|
93
118
|
tanagram [command]
|
|
94
119
|
|
|
95
120
|
COMMANDS:
|
|
96
|
-
run
|
|
97
|
-
sync
|
|
98
|
-
list
|
|
99
|
-
|
|
121
|
+
run Check git changes against policies (default)
|
|
122
|
+
sync Manually sync instruction files to cache
|
|
123
|
+
list Show all cached policies
|
|
124
|
+
config claude Setup Claude Code hook automatically
|
|
125
|
+
config list Show hook installation status
|
|
126
|
+
help Show this help message
|
|
100
127
|
|
|
101
128
|
EXAMPLES:
|
|
102
|
-
tanagram
|
|
103
|
-
tanagram run
|
|
104
|
-
tanagram sync
|
|
105
|
-
tanagram list
|
|
129
|
+
tanagram # Check changes (auto-syncs if files changed)
|
|
130
|
+
tanagram run # Same as above
|
|
131
|
+
tanagram sync # Manually sync policies
|
|
132
|
+
tanagram list # View all cached policies
|
|
133
|
+
tanagram config claude # Setup Claude Code hook in ~/.claude/settings.json
|
|
134
|
+
tanagram config list # Show where hooks are installed
|
|
106
135
|
|
|
107
136
|
INSTRUCTION FILES:
|
|
108
137
|
Tanagram looks for instruction files in your git repository:
|