@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.
@@ -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 Check git changes against policies (default)
97
- sync Manually sync instruction files to cache
98
- list Show all cached policies
99
- help Show this help message
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 # Check changes (auto-syncs if files changed)
103
- tanagram run # Same as above
104
- tanagram sync # Manually sync policies
105
- tanagram list # View all cached policies
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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanagram/cli",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
4
4
  "description": "Tanagram - Catch sloppy code before it ships",
5
5
  "main": "index.js",
6
6
  "bin": {