claude-git-hooks 1.5.4 → 2.0.0

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.
@@ -1,432 +1,79 @@
1
1
  #!/bin/bash
2
2
 
3
- # Git Pre-commit Hook for code evaluation with Claude CLI
4
- # Archivo: .git/hooks/pre-commit
3
+ # Git Pre-commit Hook - Node.js Wrapper
4
+ # This is a minimal wrapper that calls the actual Node.js implementation
5
+ # Why: Allows the Node.js script to use relative imports from its installed location
5
6
 
6
7
  set -e
7
8
 
8
- # Configuration
9
- CLAUDE_CLI="claude"
10
- TEMP_DIR="/tmp/code-review-$$"
11
- MAX_FILE_SIZE=100000 # 100KB maximum per file
12
-
13
9
  # Colors for output
14
10
  RED='\033[0;31m'
15
- GREEN='\033[0;32m'
16
- YELLOW='\033[1;33m'
17
- BLUE='\033[0;34m'
18
- NC='\033[0m' # No Color
19
-
20
- # Function for logging
21
- log() {
22
- echo -e "${GREEN}[PRE-COMMIT]${NC} $1"
23
- }
24
-
25
- error() {
26
- echo -e "${RED}[ERROR]${NC} $1"
27
- }
28
-
29
- warning() {
30
- echo -e "${YELLOW}[WARNING]${NC} $1"
11
+ NC='\033[0m'
12
+
13
+ # Convert Windows path to Git Bash/WSL path
14
+ # Why: npm prefix -g in Git Bash returns Windows paths (C:\...) that need conversion
15
+ convert_windows_path() {
16
+ local path="$1"
17
+
18
+ # Check if it's a Windows path (contains :\ or starts with C:)
19
+ if [[ "$path" =~ ^[A-Za-z]:\\ ]] || [[ "$path" =~ ^[A-Za-z]: ]]; then
20
+ # Convert C:\path\to\file to /c/path/to/file
21
+ # First, extract drive letter
22
+ local drive=$(echo "$path" | sed 's/^\([A-Za-z]\):.*/\1/' | tr '[:upper:]' '[:lower:]')
23
+ # Remove drive letter and colon, replace backslashes with forward slashes
24
+ local rest=$(echo "$path" | sed 's/^[A-Za-z]://' | sed 's/\\/\//g')
25
+ echo "/$drive$rest"
26
+ else
27
+ echo "$path"
28
+ fi
31
29
  }
32
30
 
33
- # Check version at start (before any analysis)
34
- # Try to find the check-version.sh script
35
- CHECK_VERSION_SCRIPT=""
36
- # Search in multiple possible locations
37
- SCRIPT_PATHS=(
38
- "$(dirname "$0")/check-version.sh"
39
- "/usr/local/lib/node_modules/claude-git-hooks/templates/check-version.sh"
40
- "/usr/lib/node_modules/claude-git-hooks/templates/check-version.sh"
41
- "$HOME/.npm-global/lib/node_modules/claude-git-hooks/templates/check-version.sh"
42
- "$(npm prefix -g 2>/dev/null)/lib/node_modules/claude-git-hooks/templates/check-version.sh"
43
- )
31
+ # Function to find the Node.js script
32
+ find_hook_script() {
33
+ # Why: Try multiple locations to find where npm installed the package
34
+ # Checks: global npm, local node_modules, npm prefix
44
35
 
45
- for path in "${SCRIPT_PATHS[@]}"; do
46
- if [ -f "$path" ]; then
47
- CHECK_VERSION_SCRIPT="$path"
48
- break
36
+ # Get npm global prefix and convert if it's a Windows path
37
+ local npm_prefix=$(npm prefix -g 2>/dev/null || echo "")
38
+ if [ -n "$npm_prefix" ]; then
39
+ npm_prefix=$(convert_windows_path "$npm_prefix")
49
40
  fi
50
- done
51
-
52
- # If we find the script, execute it
53
- if [ -n "$CHECK_VERSION_SCRIPT" ]; then
54
- # Source the script to have access to the function
55
- source "$CHECK_VERSION_SCRIPT"
56
- check_version
57
- fi
58
41
 
59
- # Function to generate AI-friendly resolution prompt
60
- generate_resolution_prompt() {
61
- local RESOLUTION_FILE="./claude_resolution_prompt.md"
62
- local RESOLUTION_TEMPLATE=".claude/CLAUDE_RESOLUTION_PROMPT.md"
63
-
64
- if [ ! -f "$RESOLUTION_TEMPLATE" ]; then
65
- warning "Resolution template not found: $RESOLUTION_TEMPLATE"
66
- return
67
- fi
68
-
69
- # Get context information
70
- local REPO_NAME=$(basename $(git rev-parse --show-toplevel))
71
- local BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
72
- local COMMIT_SHA="pending"
73
- local FILE_COUNT=$(echo "$JAVA_FILES" | wc -l)
74
-
75
- # Format the blocking issues for the resolution prompt
76
- local ISSUES_FORMATTED=""
77
- local issue_num=1
78
-
79
- # Use a temporary file to accumulate the issues
80
- local TEMP_ISSUES=$(mktemp)
81
-
82
- # Parse each blocking issue as JSON object
83
- echo "$JSON_RESPONSE" | jq -c '.blockingIssues[]?' 2>/dev/null | while IFS= read -r issue; do
84
- if [ -n "$issue" ]; then
85
- local desc=$(echo "$issue" | jq -r '.description')
86
- local file=$(echo "$issue" | jq -r '.file')
87
- local line=$(echo "$issue" | jq -r '.line')
88
- local method=$(echo "$issue" | jq -r '.method')
89
- local severity=$(echo "$issue" | jq -r '.severity')
90
-
91
- echo "### Issue #${issue_num} [${severity^^}]" >> "$TEMP_ISSUES"
92
- echo "**Description:** ${desc}" >> "$TEMP_ISSUES"
93
- echo "**Location:** ${file}:${line}" >> "$TEMP_ISSUES"
94
- echo "**Method/Class:** ${method}" >> "$TEMP_ISSUES"
95
- echo "" >> "$TEMP_ISSUES"
96
- issue_num=$((issue_num + 1))
42
+ local SCRIPT_PATHS=(
43
+ # Global npm installation (Linux/Mac)
44
+ "/usr/local/lib/node_modules/claude-git-hooks/lib/hooks/pre-commit.js"
45
+ "/usr/lib/node_modules/claude-git-hooks/lib/hooks/pre-commit.js"
46
+ # Global npm installation (Windows) - node_modules directly under prefix
47
+ "${npm_prefix}/node_modules/claude-git-hooks/lib/hooks/pre-commit.js"
48
+ # Global npm installation (Unix/WSL) - lib/node_modules under prefix
49
+ "${npm_prefix}/lib/node_modules/claude-git-hooks/lib/hooks/pre-commit.js"
50
+ # Home directory npm global
51
+ "$HOME/.npm-global/lib/node_modules/claude-git-hooks/lib/hooks/pre-commit.js"
52
+ # Local node_modules (if linked or installed locally)
53
+ "./node_modules/claude-git-hooks/lib/hooks/pre-commit.js"
54
+ "../node_modules/claude-git-hooks/lib/hooks/pre-commit.js"
55
+ )
56
+
57
+ for path in "${SCRIPT_PATHS[@]}"; do
58
+ if [ -f "$path" ]; then
59
+ echo "$path"
60
+ return 0
97
61
  fi
98
62
  done
99
-
100
- # Read the accumulated content
101
- ISSUES_FORMATTED=$(cat "$TEMP_ISSUES")
102
- rm -f "$TEMP_ISSUES"
103
-
104
- # Generate the prompt from the template
105
- cp "$RESOLUTION_TEMPLATE" "$RESOLUTION_FILE"
106
-
107
- # Replace placeholders - use double quotes and escape special characters
108
- sed -i "s|{{REPO_NAME}}|${REPO_NAME}|g" "$RESOLUTION_FILE"
109
- sed -i "s|{{BRANCH_NAME}}|${BRANCH_NAME}|g" "$RESOLUTION_FILE"
110
- sed -i "s|{{COMMIT_SHA}}|${COMMIT_SHA}|g" "$RESOLUTION_FILE"
111
- sed -i "s|{{FILE_COUNT}}|${FILE_COUNT}|g" "$RESOLUTION_FILE"
112
-
113
- # Create temporary file for formatted issues
114
- local TEMP_ISSUES_FILE=$(mktemp)
115
- echo "$ISSUES_FORMATTED" > "$TEMP_ISSUES_FILE"
116
-
117
- # Replace {{BLOCKING_ISSUES}} with content
118
- if [ -n "$ISSUES_FORMATTED" ]; then
119
- sed -i "/{{BLOCKING_ISSUES}}/r $TEMP_ISSUES_FILE" "$RESOLUTION_FILE"
120
- fi
121
- sed -i "s|{{BLOCKING_ISSUES}}||g" "$RESOLUTION_FILE"
122
- rm -f "$TEMP_ISSUES_FILE"
123
-
124
- # Add content from affected files
125
- local TEMP_FILES=$(mktemp)
126
- echo "$JSON_RESPONSE" | jq -r '.blockingIssues[].file' 2>/dev/null | sort -u | while IFS= read -r file; do
127
- if [ -f "$file" ]; then
128
- echo "### File: $file" >> "$TEMP_FILES"
129
- echo "" >> "$TEMP_FILES"
130
- echo '```' >> "$TEMP_FILES"
131
- cat "$file" >> "$TEMP_FILES"
132
- echo '```' >> "$TEMP_FILES"
133
- echo "" >> "$TEMP_FILES"
134
- fi
135
- done
136
-
137
- # Replace {{FILE_CONTENTS}} with content
138
- if [ -s "$TEMP_FILES" ]; then
139
- sed -i "/{{FILE_CONTENTS}}/r $TEMP_FILES" "$RESOLUTION_FILE"
140
- fi
141
- sed -i "s|{{FILE_CONTENTS}}||g" "$RESOLUTION_FILE"
142
- rm -f "$TEMP_FILES"
143
-
144
- echo
145
- echo -e "${YELLOW}=== AI RESOLUTION PROMPT GENERATED ===${NC}"
146
- echo -e "${GREEN}An AI-friendly prompt has been generated at: ${BLUE}$RESOLUTION_FILE${NC}"
147
- echo -e "${YELLOW}Copy this file to a new Claude instance to resolve problems automatically.${NC}"
148
- echo
149
- }
150
-
151
- # Configure files for SonarQube mode
152
- GUIDELINES_FILE=".claude/CLAUDE_PRE_COMMIT_SONAR.md"
153
- PROMPT_TEMPLATE=".claude/CLAUDE_ANALYSIS_PROMPT_SONAR.md"
154
63
 
155
-
156
- # Check that the prompt template exists
157
- if [ ! -f "$PROMPT_TEMPLATE" ]; then
158
- error "Prompt template not found: $PROMPT_TEMPLATE"
159
- error "Claude configuration files appear to be incomplete."
160
- error "Please reinstall claude-git-hooks by running:"
161
- error " claude-hooks install --force"
162
- exit 1
163
- fi
164
- # Function to clean temporary files
165
- cleanup() {
166
- rm -rf "$TEMP_DIR"
167
- }
168
-
169
- # Configure cleanup on exit
170
- trap cleanup EXIT
171
-
172
- # Create temporary directory
173
- mkdir -p "$TEMP_DIR"
174
-
175
- # Check if Claude CLI is installed (only for code analysis)
176
- if ! command -v "$CLAUDE_CLI" &> /dev/null; then
177
- error "Claude CLI is not installed or not found in PATH"
178
- error "Install Claude CLI from: https://github.com/anthropics/claude-cli"
179
- exit 1
180
- fi
181
-
182
- # Now check if there are Java files to analyze
183
- JAVA_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(java|xml|properties|yml|yaml)$' || true)
184
-
185
- if [ -z "$JAVA_FILES" ]; then
186
- log "No Java/configuration files to review"
187
- exit 0
188
- fi
189
-
190
- # Check if the guidelines file exists
191
- if [ ! -f "$GUIDELINES_FILE" ]; then
192
- error "Guidelines file not found: $GUIDELINES_FILE"
193
- error "Please reinstall claude-git-hooks by running:"
194
- error " claude-hooks install --force"
195
- exit 1
196
- fi
197
-
198
- log "Java/config files to review: $(echo "$JAVA_FILES" | wc -l)"
199
-
200
- # Function to filter content with SKIP-ANALYSIS
201
- filter_skip_analysis() {
202
- local file_content="$1"
203
- local filtered_content=""
204
- local skip_next_line=false
205
- local inside_skip_block=false
206
-
207
- while IFS= read -r line; do
208
- # Detect SKIP-ANALYSIS for single line
209
- if echo "$line" | grep -q "// SKIP-ANALYSIS"; then
210
- skip_next_line=true
211
- continue
212
- fi
213
-
214
- # Detect start/end of SKIP_ANALYSIS_BLOCK
215
- if echo "$line" | grep -q "// SKIP_ANALYSIS_BLOCK"; then
216
- if [ "$inside_skip_block" = true ]; then
217
- # End of block
218
- inside_skip_block=false
219
- else
220
- # Start of block
221
- inside_skip_block=true
222
- fi
223
- continue
224
- fi
225
-
226
- # If we're inside a block, skip the line
227
- if [ "$inside_skip_block" = true ]; then
228
- continue
229
- fi
230
-
231
- # If we should skip the next line (single comment)
232
- if [ "$skip_next_line" = true ]; then
233
- skip_next_line=false
234
- continue
235
- fi
236
-
237
- # Add line to filtered content
238
- filtered_content="${filtered_content}${line}"$'\n'
239
- done <<< "$file_content"
240
-
241
- echo "$filtered_content"
64
+ return 1
242
65
  }
243
66
 
244
- # Build the prompt for code analysis
245
- PROMPT_FILE="$TEMP_DIR/code_review_prompt.txt"
246
-
247
- # Copy the prompt template
248
- cat "$PROMPT_TEMPLATE" > "$PROMPT_FILE"
249
-
250
- # Add the guidelines
251
- echo "=== EVALUATION GUIDELINES ===" >> "$PROMPT_FILE"
252
- cat "$GUIDELINES_FILE" >> "$PROMPT_FILE"
253
- echo -e "\n\n=== CHANGES TO REVIEW ===\n" >> "$PROMPT_FILE"
254
-
255
- # Process each Java file
256
- FILE_COUNT=0
257
- for FILE in $JAVA_FILES; do
258
- if [ -f "$FILE" ]; then
259
- FILE_SIZE=$(stat -c%s "$FILE" 2>/dev/null || stat -f%z "$FILE" 2>/dev/null || echo "0")
260
-
261
- if [ "$FILE_SIZE" -gt "$MAX_FILE_SIZE" ]; then
262
- warning "File $FILE too large ($FILE_SIZE bytes), skipping..."
263
- continue
264
- fi
265
-
266
- echo -e "\n--- Archivo: $FILE ---" >> "$PROMPT_FILE"
267
-
268
- # Get the diff and filter it
269
- DIFF_CONTENT=$(git diff --cached "$FILE" 2>/dev/null || echo "Could not get diff")
270
- FILTERED_DIFF=$(filter_skip_analysis "$DIFF_CONTENT")
271
-
272
- # Show the filtered diff of the file
273
- echo -e "\nDiff:" >> "$PROMPT_FILE"
274
- echo "$FILTERED_DIFF" >> "$PROMPT_FILE"
275
-
276
- # If it's a new file, show complete filtered content
277
- if git diff --cached --name-status | grep "^A.*$FILE" > /dev/null; then
278
- echo -e "\nComplete content (new file):" >> "$PROMPT_FILE"
279
- FILE_CONTENT=$(git show ":$FILE" 2>/dev/null || cat "$FILE")
280
- FILTERED_CONTENT=$(filter_skip_analysis "$FILE_CONTENT")
281
- echo "$FILTERED_CONTENT" >> "$PROMPT_FILE"
282
- fi
283
-
284
- FILE_COUNT=$((FILE_COUNT + 1))
285
- fi
286
- done
287
-
288
- if [ "$FILE_COUNT" -eq 0 ]; then
289
- log "No valid files found to review"
290
- exit 0
291
- fi
292
-
293
- if [ "$FILE_COUNT" -gt 10 ]; then
294
- warning "Too many files to review ($FILE_COUNT)"
295
- warning "Consider splitting the commit into smaller parts"
296
- exit 0
297
- fi
298
-
299
- log "Sending $FILE_COUNT files for review with Claude..."
300
-
301
- # Send to Claude and capture response
302
- RESPONSE_FILE="$TEMP_DIR/code_review_response.txt"
303
-
304
- # Execute Claude CLI and capture the response
305
- if $CLAUDE_CLI < "$PROMPT_FILE" > "$RESPONSE_FILE" 2>&1; then
306
- # Extract the JSON from the response
307
- JSON_RESPONSE=$(sed -n '/^{/,/^}/p' "$RESPONSE_FILE" | head -n 1000)
308
-
309
- if [ -z "$JSON_RESPONSE" ]; then
310
- error "Did not receive a valid JSON response from Claude"
311
- error "Complete response:"
312
- cat "$RESPONSE_FILE"
313
- exit 1
314
- fi
315
-
316
- # Save JSON for debug if activated
317
- if [ -n "$DEBUG" ]; then
318
- echo "$JSON_RESPONSE" > ./debug-claude-response.json
319
- log "Response saved to debug-claude-response.json"
320
- fi
321
-
322
- # Parse the response using jq
323
- APPROVED=$(echo "$JSON_RESPONSE" | jq -r '.approved // false')
324
- SCORE=$(echo "$JSON_RESPONSE" | jq -r '.score // 0')
325
- RECOMMENDATIONS=$(echo "$JSON_RESPONSE" | jq -r '.recommendations[]?' 2>/dev/null | sed '/^$/d')
326
-
327
- # Parse blockingIssues as objects and extract descriptions
328
- BLOCKING_ISSUES=""
329
- BLOCKING_COUNT=$(echo "$JSON_RESPONSE" | jq '.blockingIssues | length' 2>/dev/null || echo "0")
330
-
331
- if [ "$BLOCKING_COUNT" -gt 0 ]; then
332
- BLOCKING_ISSUES=$(echo "$JSON_RESPONSE" | jq -r '.blockingIssues[].description' 2>/dev/null | sed '/^$/d')
333
- fi
334
-
335
- # Always use SonarQube mode (as per v1.4.1)
336
- QUALITY_GATE=$(echo "$JSON_RESPONSE" | jq -r '.QUALITY_GATE // ""' 2>/dev/null)
337
-
338
- # Show SonarQube style results
339
- echo
340
- echo "╔════════════════════════════════════════════════════════════════════╗"
341
- echo "║ CODE QUALITY ANALYSIS ║"
342
- echo "╚════════════════════════════════════════════════════════════════════╝"
343
- echo
344
-
345
- # Quality Gate Status
346
- if [ "$QUALITY_GATE" = "PASSED" ]; then
347
- echo -e "${GREEN}✓ Quality Gate: PASSED${NC}"
348
- else
349
- echo -e "${RED}✗ Quality Gate: FAILED${NC}"
350
- fi
351
- echo
352
-
353
- # Metrics
354
- METRICS=$(echo "$JSON_RESPONSE" | jq -r '.metrics // {}' 2>/dev/null)
355
- if [ "$METRICS" != "{}" ] && [ "$METRICS" != "null" ]; then
356
- echo "📊 METRICS"
357
- echo "├─ Reliability: $(echo "$METRICS" | jq -r '.reliability // "?"' 2>/dev/null)"
358
- echo "├─ Security: $(echo "$METRICS" | jq -r '.security // "?"' 2>/dev/null)"
359
- echo "├─ Maintainability: $(echo "$METRICS" | jq -r '.maintainability // "?"' 2>/dev/null)"
360
- echo "├─ Coverage: $(echo "$METRICS" | jq -r '.coverage // "?"' 2>/dev/null)%"
361
- echo "├─ Duplications: $(echo "$METRICS" | jq -r '.duplications // "?"' 2>/dev/null)%"
362
- echo "└─ Complexity: $(echo "$METRICS" | jq -r '.complexity // "?"' 2>/dev/null)"
363
- echo
364
- fi
365
-
366
- # Issues Summary
367
- ISSUES=$(echo "$JSON_RESPONSE" | jq -r '.issues // {}' 2>/dev/null)
368
- if [ "$ISSUES" != "{}" ] && [ "$ISSUES" != "null" ]; then
369
- echo "📋 ISSUES SUMMARY"
370
- BLOCKER=$(echo "$ISSUES" | jq -r '.blocker // 0' 2>/dev/null)
371
- CRITICAL=$(echo "$ISSUES" | jq -r '.critical // 0' 2>/dev/null)
372
- MAJOR=$(echo "$ISSUES" | jq -r '.major // 0' 2>/dev/null)
373
- MINOR=$(echo "$ISSUES" | jq -r '.minor // 0' 2>/dev/null)
374
- INFO=$(echo "$ISSUES" | jq -r '.info // 0' 2>/dev/null)
375
- TOTAL=$((BLOCKER + CRITICAL + MAJOR + MINOR + INFO))
376
-
377
- echo "Total: $TOTAL issues found"
378
- [ "$BLOCKER" -gt 0 ] && echo -e " ${RED}🔴 Blocker: $BLOCKER${NC}"
379
- [ "$CRITICAL" -gt 0 ] && echo -e " ${RED}🟠 Critical: $CRITICAL${NC}"
380
- [ "$MAJOR" -gt 0 ] && echo -e " ${YELLOW}🟡 Major: $MAJOR${NC}"
381
- [ "$MINOR" -gt 0 ] && echo " 🔵 Minor: $MINOR"
382
- [ "$INFO" -gt 0 ] && echo " ⚪ Info: $INFO"
383
- echo
384
- fi
385
-
386
- # Detailed Issues
387
- DETAILS_COUNT=$(echo "$JSON_RESPONSE" | jq -r '.details | length' 2>/dev/null)
388
- if [ "$DETAILS_COUNT" -gt 0 ] 2>/dev/null; then
389
- echo "🔍 DETAILED ISSUES"
390
- echo "$JSON_RESPONSE" | jq -r '.details[]? |
391
- "[\(.severity)] \(.type) in \(.file):\(.line // "?")\n \(.message)\n"' 2>/dev/null
392
- fi
393
-
394
- # Security Hotspots
395
- HOTSPOTS=$(echo "$JSON_RESPONSE" | jq -r '.securityHotspots // 0' 2>/dev/null)
396
- if [ "$HOTSPOTS" -gt 0 ] 2>/dev/null; then
397
- echo "🔥 SECURITY HOTSPOTS: $HOTSPOTS found"
398
- echo " Review security-sensitive code carefully"
399
- echo
400
- fi
401
-
402
- # Check if commit should be blocked
403
- if [ "$QUALITY_GATE" = "FAILED" ] || [ "$APPROVED" = "false" ]; then
404
- echo
405
- error "❌ Commit blocked due to quality gate failure"
406
-
407
- # Show blocking issues if they exist
408
- if [ -n "$BLOCKING_ISSUES" ] && [ "$BLOCKING_ISSUES" != "null" ]; then
409
- echo
410
- echo "=== CRITICAL ISSUES ==="
411
- echo "$BLOCKING_ISSUES" | sed 's/^/- /'
412
- fi
413
-
414
- # Generate AI-friendly resolution prompt if there are blocking issues
415
- if [ "$BLOCKING_COUNT" -gt 0 ]; then
416
- generate_resolution_prompt
417
- fi
418
-
419
- exit 1
420
- fi
67
+ # Find the Node.js script
68
+ NODE_HOOK=$(find_hook_script)
421
69
 
422
- echo
423
- log " Code analysis completed. Quality gate passed."
424
-
425
- else
426
- error "Error executing Claude CLI"
427
- error "Check that Claude CLI is configured correctly"
428
- cat "$RESPONSE_FILE"
70
+ if [ -z "$NODE_HOOK" ]; then
71
+ echo -e "${RED}Error: Could not find pre-commit.js${NC}" >&2
72
+ echo "Claude Git Hooks may not be properly installed." >&2
73
+ echo "Try running: claude-hooks install --force" >&2
429
74
  exit 1
430
75
  fi
431
76
 
432
- exit 0
77
+ # Execute the Node.js script
78
+ # Why: Pass all arguments ($@) to support future hook extensions
79
+ exec node "$NODE_HOOK" "$@"