claude-git-hooks 2.12.0 → 2.14.1
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/CHANGELOG.md +54 -1
- package/README.md +52 -13
- package/bin/claude-hooks +8 -0
- package/lib/commands/analyze-diff.js +32 -153
- package/lib/commands/analyze.js +217 -0
- package/lib/commands/bump-version.js +172 -50
- package/lib/commands/create-pr.js +15 -80
- package/lib/commands/helpers.js +7 -0
- package/lib/hooks/pre-commit.js +26 -265
- package/lib/utils/analysis-engine.js +469 -0
- package/lib/utils/claude-client.js +6 -1
- package/lib/utils/claude-diagnostics.js +2 -1
- package/lib/utils/git-operations.js +537 -1
- package/lib/utils/git-tag-manager.js +58 -8
- package/lib/utils/interactive-ui.js +86 -1
- package/lib/utils/pr-metadata-engine.js +474 -0
- package/lib/utils/resolution-prompt.js +57 -34
- package/lib/utils/version-manager.js +219 -52
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,59 @@ Todos los cambios notables en este proyecto se documentarán en este archivo.
|
|
|
5
5
|
El formato está basado en [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
y este proyecto adhiere a [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.14.1] - 2026-02-06
|
|
9
|
+
|
|
10
|
+
### 🔧 Changed
|
|
11
|
+
- Improved `bump-version` command workflow - now automatically stages and commits version changes with conventional commit format before tag creation
|
|
12
|
+
- Enhanced `bump-version` command flexibility - added `--no-commit` flag for manual workflows where users prefer to commit changes themselves
|
|
13
|
+
- Improved error handling in `bump-version` - provides clearer guidance when staging or committing fails, with manual fallback instructions
|
|
14
|
+
- Enhanced next-steps guidance in `bump-version` output - adapts instructions based on flags used (--no-commit, --no-push, --no-tag)
|
|
15
|
+
|
|
16
|
+
### 🐛 Fixed
|
|
17
|
+
- Fixed `bump-version` workflow inconsistency - prevented tag creation when using `--no-commit` flag, as tags should only be created after changes are committed
|
|
18
|
+
- Fixed potential git history issues - version bump commits now use `--no-verify` flag to bypass pre-commit hooks and prevent circular execution
|
|
19
|
+
|
|
20
|
+
### 🗑️ Removed
|
|
21
|
+
- Removed redundant fallback warnings from `analyze-diff` and `create-pr` commands - warnings were unnecessary as base branch resolution now throws clear errors with suggestions instead
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## [2.14.0] - 2026-02-06
|
|
25
|
+
|
|
26
|
+
### ✨ Added
|
|
27
|
+
- PR metadata engine (`pr-metadata-engine.js`) - centralized module for generating PR titles, descriptions, and test plans from branch diffs with timeout resilience (#63)
|
|
28
|
+
- Tiered diff reduction system - automatically truncates large diffs using context reduction, proportional budgets, and stat-only summaries to prevent timeouts
|
|
29
|
+
- Extended git operations - added `fetchRemote()`, `branchExists()`, `resolveBaseBranch()`, `getChangedFilesBetweenRefs()`, `getDiffBetweenRefs()`, and `getCommitsBetweenRefs()` to `git-operations.js`
|
|
30
|
+
- RFC-001 documentation - comprehensive design specification for PR metadata engine refactor with timeout resilience strategy
|
|
31
|
+
|
|
32
|
+
### 🔧 Changed
|
|
33
|
+
- Refactored `analyze-diff` command to use shared PR metadata engine - reduced from 262 to ~80 lines by eliminating duplicate git logic
|
|
34
|
+
- Refactored `create-pr` command to use shared PR metadata engine - eliminated duplicate diff extraction logic (lines 340-420)
|
|
35
|
+
- Improved timeout error handling - timeout errors now include `errorInfo` and are classified as recoverable for automatic retry with exponential backoff
|
|
36
|
+
- Updated CLAUDE.md with PR metadata engine architecture, new git operations exports, and tiered diff reduction strategy
|
|
37
|
+
|
|
38
|
+
### 🐛 Fixed
|
|
39
|
+
- Fixed timeout failures in `analyze-diff` for large diffs - now uses tiered reduction instead of monolithic diff processing
|
|
40
|
+
- Fixed inconsistent behavior between `analyze-diff` and `create-pr` commands - both now share identical analysis logic via unified engine
|
|
41
|
+
|
|
42
|
+
### 🗑️ Removed
|
|
43
|
+
- Removed unreliable `SUBAGENT_INSTRUCTION` text hint - replaced with deterministic tiered diff reduction for parallel processing
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
## [2.13.0] - 2026-02-05
|
|
47
|
+
|
|
48
|
+
### ✨ Added
|
|
49
|
+
- Interactive analysis command `claude-hooks analyze` - review all issues (INFO to BLOCKER) interactively before committing with auto-commit option
|
|
50
|
+
- Shared analysis engine module (`analysis-engine.js`) - centralized file data building, analysis orchestration, and results display for both pre-commit hooks and interactive analysis
|
|
51
|
+
- Support for version files in subdirectories - automatically searches parent directories when version files not found in repo root
|
|
52
|
+
- Auto-commit functionality after interactive analysis - creates commit with auto-generated message when user confirms
|
|
53
|
+
|
|
54
|
+
### 🔧 Changed
|
|
55
|
+
- Improved version detection logic for pom.xml files - enhanced reliability of version extraction in Maven projects
|
|
56
|
+
|
|
57
|
+
### 🐛 Fixed
|
|
58
|
+
- Fixed version search logic for pom.xml files - corrected parsing issues in Maven version detection
|
|
59
|
+
|
|
60
|
+
|
|
8
61
|
## [2.12.0] - 2026-02-03
|
|
9
62
|
|
|
10
63
|
### ✨ Added
|
|
@@ -43,7 +96,7 @@ y este proyecto adhiere a [Semantic Versioning](https://semver.org/spec/v2.0.0.h
|
|
|
43
96
|
|
|
44
97
|
- **New utility modules**:
|
|
45
98
|
- `lib/utils/version-manager.js` - Version detection, parsing, incrementing, validation
|
|
46
|
-
- `lib/utils/git-tag-manager.js` - Git tag operations (create, list, compare, push)
|
|
99
|
+
- `lib/utils/git-tag-manager.js` - Git tag operations (create, list, compare, push, isSemverTag)
|
|
47
100
|
- `lib/utils/changelog-generator.js` - CHANGELOG generation with Claude
|
|
48
101
|
- `templates/GENERATE_CHANGELOG.md` - Claude prompt for changelog analysis
|
|
49
102
|
|
package/README.md
CHANGED
|
@@ -60,6 +60,33 @@ export GITHUB_TOKEN="ghp_..."
|
|
|
60
60
|
|
|
61
61
|
Create token at https://github.com/settings/tokens with scopes: `repo`, `read:org`
|
|
62
62
|
|
|
63
|
+
### Analyze Code (Interactive Review)
|
|
64
|
+
|
|
65
|
+
Run interactive code analysis before committing:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# Analyze staged changes (default)
|
|
69
|
+
claude-hooks analyze
|
|
70
|
+
|
|
71
|
+
# Analyze unstaged changes
|
|
72
|
+
claude-hooks analyze --unstaged
|
|
73
|
+
|
|
74
|
+
# Analyze all tracked files
|
|
75
|
+
claude-hooks analyze --all
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**What it does:**
|
|
79
|
+
- Analyzes selected file scope (staged, unstaged, or all)
|
|
80
|
+
- Shows all issues (INFO, MINOR, MAJOR, CRITICAL, BLOCKER)
|
|
81
|
+
- Interactive prompt with options:
|
|
82
|
+
- **Continue**: Creates commit automatically with auto-generated message
|
|
83
|
+
- **Abort**: Generate resolution prompt and fix issues
|
|
84
|
+
- **View**: Show detailed issue list
|
|
85
|
+
- Executes `git commit -m "auto" --no-verify` on confirmation
|
|
86
|
+
- Works outside git hooks (no stdin limitations)
|
|
87
|
+
|
|
88
|
+
**Use case:** Complete analysis-to-commit workflow in one command.
|
|
89
|
+
|
|
63
90
|
### Analyze Diff (without creating PR)
|
|
64
91
|
|
|
65
92
|
```bash
|
|
@@ -83,12 +110,16 @@ claude-hooks bump-version major --update-changelog
|
|
|
83
110
|
|
|
84
111
|
# Preview without applying
|
|
85
112
|
claude-hooks bump-version patch --dry-run
|
|
113
|
+
|
|
114
|
+
# Manual workflow (skip automatic commit)
|
|
115
|
+
claude-hooks bump-version patch --no-commit
|
|
86
116
|
```
|
|
87
117
|
|
|
88
118
|
**What it does:**
|
|
89
119
|
- Detects project type (Node.js, Maven, or monorepo with both)
|
|
90
120
|
- Updates `package.json` and/or `pom.xml`
|
|
91
121
|
- Generates CHANGELOG entry with Claude (analyzes commits)
|
|
122
|
+
- Commits changes automatically with conventional commit format
|
|
92
123
|
- Creates annotated Git tag with `v` prefix (e.g., `v2.7.0`)
|
|
93
124
|
- Pushes tag to remote automatically
|
|
94
125
|
|
|
@@ -223,9 +254,11 @@ claude-hooks --help # Full command reference
|
|
|
223
254
|
|
|
224
255
|
| Module | Purpose | Key Exports |
|
|
225
256
|
|--------|---------|-------------|
|
|
257
|
+
| `analysis-engine.js` | **Shared analysis logic** - file data, orchestration, results (v2.13.0) | `buildFilesData()`, `runAnalysis()`, `consolidateResults()`, `displayResults()` |
|
|
226
258
|
| `claude-client.js` | **Claude CLI wrapper** - spawn, retry, parallel execution | `analyzeCode()`, `analyzeCodeParallel()`, `executeClaudeWithRetry()` |
|
|
227
259
|
| `prompt-builder.js` | **Prompt construction** - load templates, replace variables | `buildAnalysisPrompt()`, `loadPrompt()` |
|
|
228
|
-
| `git-operations.js` | **Git abstractions** - staged files, diff,
|
|
260
|
+
| `git-operations.js` | **Git abstractions** - staged files, diff, branch comparison | `getStagedFiles()`, `getDiff()`, `getRepoRoot()`, `resolveBaseBranch()`, `getDiffBetweenRefs()` |
|
|
261
|
+
| `pr-metadata-engine.js` | **PR metadata generation** - branch context, diff reduction (v2.14.0) | `getBranchContext()`, `buildDiffPayload()`, `generatePRMetadata()`, `analyzeBranchForPR()` |
|
|
229
262
|
| `github-api.js` | **Octokit integration** - PR creation, token validation | `createPullRequest()`, `validateToken()`, `saveGitHubToken()` |
|
|
230
263
|
| `github-client.js` | **GitHub helpers** - CODEOWNERS parsing, reviewers | `getReviewersForFiles()`, `parseGitHubRepo()` |
|
|
231
264
|
| `preset-loader.js` | **Preset system** - load tech-stack configurations | `loadPreset()`, `listPresets()` |
|
|
@@ -242,12 +275,16 @@ claude-hooks --help # Full command reference
|
|
|
242
275
|
```
|
|
243
276
|
git commit → templates/pre-commit (bash wrapper)
|
|
244
277
|
→ lib/hooks/pre-commit.js
|
|
245
|
-
→ getStagedFiles() →
|
|
246
|
-
→
|
|
247
|
-
→
|
|
248
|
-
→ if
|
|
278
|
+
→ getStagedFiles() → filterFiles() by preset extensions + size
|
|
279
|
+
→ buildFilesData() → runAnalysis() (via analysis-engine.js)
|
|
280
|
+
→ displayResults() → show quality gate status
|
|
281
|
+
→ if blocking issues (critical/blocker):
|
|
282
|
+
generates claude_resolution_prompt.md → exit 1 (block)
|
|
283
|
+
→ if non-blocking or no issues: exit 0 (pass)
|
|
249
284
|
```
|
|
250
285
|
|
|
286
|
+
**Note:** For interactive review of non-blocking issues, use `claude-hooks analyze` before committing.
|
|
287
|
+
|
|
251
288
|
#### Commit Message Generation
|
|
252
289
|
|
|
253
290
|
```
|
|
@@ -258,16 +295,18 @@ git commit -m "auto" → templates/prepare-commit-msg (bash wrapper)
|
|
|
258
295
|
→ write to COMMIT_EDITMSG
|
|
259
296
|
```
|
|
260
297
|
|
|
261
|
-
#### PR
|
|
298
|
+
#### PR Metadata Generation (analyze-diff / create-pr)
|
|
262
299
|
|
|
263
300
|
```
|
|
264
|
-
claude-hooks create-pr → bin/claude-hooks (router)
|
|
265
|
-
→ lib/commands/create-pr.js
|
|
266
|
-
→
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
301
|
+
claude-hooks analyze-diff|create-pr → bin/claude-hooks (router)
|
|
302
|
+
→ lib/commands/analyze-diff.js or create-pr.js (thin wrapper)
|
|
303
|
+
→ analyzeBranchForPR() (pr-metadata-engine.js)
|
|
304
|
+
→ resolveBaseBranch() (git-operations.js)
|
|
305
|
+
→ getDiffBetweenRefs() + getCommitsBetweenRefs()
|
|
306
|
+
→ buildDiffPayload() with tiered reduction (context → proportional → stat-only)
|
|
307
|
+
→ executeClaudeWithRetry() → PRMetadata
|
|
308
|
+
→ analyze-diff: formats to console + saves JSON
|
|
309
|
+
→ create-pr: additionally creates PR via Octokit API
|
|
271
310
|
```
|
|
272
311
|
|
|
273
312
|
### Config Priority
|
package/bin/claude-hooks
CHANGED
|
@@ -13,6 +13,7 @@ import { error } from '../lib/commands/helpers.js';
|
|
|
13
13
|
// Import commands
|
|
14
14
|
import { runInstall } from '../lib/commands/install.js';
|
|
15
15
|
import { runEnable, runDisable, runStatus, runUninstall } from '../lib/commands/hooks.js';
|
|
16
|
+
import { runAnalyze } from '../lib/commands/analyze.js';
|
|
16
17
|
import { runAnalyzeDiff } from '../lib/commands/analyze-diff.js';
|
|
17
18
|
import { runCreatePr } from '../lib/commands/create-pr.js';
|
|
18
19
|
import { runSetupGitHub } from '../lib/commands/setup-github.js';
|
|
@@ -51,6 +52,13 @@ async function main() {
|
|
|
51
52
|
case 'status':
|
|
52
53
|
runStatus();
|
|
53
54
|
break;
|
|
55
|
+
case 'analyze':
|
|
56
|
+
await runAnalyze({
|
|
57
|
+
staged: !args.includes('--unstaged') && !args.includes('--all'),
|
|
58
|
+
unstaged: args.includes('--unstaged'),
|
|
59
|
+
all: args.includes('--all')
|
|
60
|
+
});
|
|
61
|
+
break;
|
|
54
62
|
case 'analyze-diff':
|
|
55
63
|
await runAnalyzeDiff(args.slice(1));
|
|
56
64
|
break;
|
|
@@ -3,15 +3,13 @@
|
|
|
3
3
|
* Purpose: Analyze differences between branches and generate PR info
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { execSync } from 'child_process';
|
|
7
6
|
import fs from 'fs';
|
|
8
|
-
import {
|
|
9
|
-
import { loadPrompt } from '../utils/prompt-builder.js';
|
|
7
|
+
import { analyzeBranchForPR } from '../utils/pr-metadata-engine.js';
|
|
10
8
|
import { getConfig } from '../config.js';
|
|
9
|
+
import logger from '../utils/logger.js';
|
|
11
10
|
import {
|
|
12
11
|
colors,
|
|
13
12
|
error,
|
|
14
|
-
success,
|
|
15
13
|
info,
|
|
16
14
|
warning,
|
|
17
15
|
checkGitRepo
|
|
@@ -27,156 +25,38 @@ export async function runAnalyzeDiff(args) {
|
|
|
27
25
|
return;
|
|
28
26
|
}
|
|
29
27
|
|
|
30
|
-
//
|
|
28
|
+
// Enable debug mode from config
|
|
31
29
|
const config = await getConfig();
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (!currentBranch) {
|
|
36
|
-
error('You are not in a valid branch.');
|
|
37
|
-
return;
|
|
30
|
+
if (config.system?.debug) {
|
|
31
|
+
logger.setDebugMode(true);
|
|
38
32
|
}
|
|
39
33
|
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
let baseBranch, compareWith, contextDescription;
|
|
44
|
-
|
|
45
|
-
if (args[0]) {
|
|
46
|
-
// Case with argument: compare current branch vs origin/specified-branch
|
|
47
|
-
const targetBranch = args[0];
|
|
48
|
-
baseBranch = `origin/${targetBranch}`;
|
|
49
|
-
compareWith = `${baseBranch}...HEAD`;
|
|
50
|
-
contextDescription = `${currentBranch} vs ${baseBranch}`;
|
|
51
|
-
|
|
52
|
-
// Check that the origin branch exists
|
|
53
|
-
try {
|
|
54
|
-
execSync(`git rev-parse --verify ${baseBranch}`, { stdio: 'ignore' });
|
|
55
|
-
} catch (e) {
|
|
56
|
-
error(`Branch ${baseBranch} does not exist.`);
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
} else {
|
|
60
|
-
// Case without argument: compare current branch vs origin/current-branch
|
|
61
|
-
baseBranch = `origin/${currentBranch}`;
|
|
62
|
-
compareWith = `${baseBranch}...HEAD`;
|
|
63
|
-
contextDescription = `${currentBranch} vs ${baseBranch}`;
|
|
64
|
-
|
|
65
|
-
// Check that the origin branch exists
|
|
66
|
-
try {
|
|
67
|
-
execSync(`git rev-parse --verify ${baseBranch}`, { stdio: 'ignore' });
|
|
68
|
-
} catch (e) {
|
|
69
|
-
// Try fallback to origin/develop
|
|
70
|
-
baseBranch = 'origin/develop';
|
|
71
|
-
compareWith = `${baseBranch}...HEAD`;
|
|
72
|
-
contextDescription = `${currentBranch} vs ${baseBranch} (fallback)`;
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
execSync(`git rev-parse --verify ${baseBranch}`, { stdio: 'ignore' });
|
|
76
|
-
warning(`Branch origin/${currentBranch} does not exist. Using ${baseBranch} as fallback.`);
|
|
77
|
-
} catch (e2) {
|
|
78
|
-
// Try fallback to origin/main
|
|
79
|
-
baseBranch = 'origin/main';
|
|
80
|
-
compareWith = `${baseBranch}...HEAD`;
|
|
81
|
-
contextDescription = `${currentBranch} vs ${baseBranch} (fallback)`;
|
|
34
|
+
// Parse target branch from arguments
|
|
35
|
+
const targetBranch = args[0];
|
|
82
36
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
warning(`No origin/develop branch. Using ${baseBranch} as fallback.`);
|
|
86
|
-
} catch (e3) {
|
|
87
|
-
error('Could not find a valid comparison branch (tried origin/current, origin/develop, origin/main).');
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
info(`Analyzing: ${contextDescription}...`);
|
|
37
|
+
info(targetBranch ? `Analyzing differences with ${targetBranch}...` : 'Analyzing differences...');
|
|
38
|
+
const startTime = Date.now();
|
|
95
39
|
|
|
96
|
-
// Get modified files
|
|
97
|
-
let diffFiles;
|
|
98
40
|
try {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const stagedFiles = execSync('git diff --cached --name-only', { encoding: 'utf8' }).trim();
|
|
104
|
-
const unstagedFiles = execSync('git diff --name-only', { encoding: 'utf8' }).trim();
|
|
41
|
+
// Call PR metadata engine
|
|
42
|
+
const { success: engineSuccess, result, error: engineError } = await analyzeBranchForPR(targetBranch, {
|
|
43
|
+
hook: 'analyze-diff'
|
|
44
|
+
});
|
|
105
45
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
console.log('Staged changes:', stagedFiles || 'none');
|
|
109
|
-
console.log('Unstaged changes:', unstagedFiles || 'none');
|
|
110
|
-
} else {
|
|
111
|
-
success('✅ No differences. Your branch is synchronized.');
|
|
112
|
-
}
|
|
46
|
+
if (!engineSuccess) {
|
|
47
|
+
error(engineError || 'Failed to analyze branch');
|
|
113
48
|
return;
|
|
114
49
|
}
|
|
115
|
-
} catch (e) {
|
|
116
|
-
error('Error getting differences: ' + e.message);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Get the complete diff
|
|
121
|
-
let fullDiff, commits;
|
|
122
|
-
try {
|
|
123
|
-
fullDiff = execSync(`git diff ${compareWith}`, { encoding: 'utf8' });
|
|
124
|
-
commits = execSync(`git log ${baseBranch}..HEAD --oneline`, { encoding: 'utf8' }).trim();
|
|
125
|
-
} catch (e) {
|
|
126
|
-
error('Error getting diff or commits: ' + e.message);
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Check if subagents should be used
|
|
131
|
-
const useSubagents = config.subagents.enabled;
|
|
132
|
-
const subagentModel = config.subagents.model;
|
|
133
|
-
let subagentBatchSize = config.subagents.batchSize;
|
|
134
|
-
// Validate batch size (must be >= 1)
|
|
135
|
-
if (subagentBatchSize < 1) {
|
|
136
|
-
subagentBatchSize = 1;
|
|
137
|
-
}
|
|
138
|
-
const subagentInstruction = useSubagents
|
|
139
|
-
? `\n\nIMPORTANT PARALLEL PROCESSING: If analyzing 3+ files, process them in batches of ${subagentBatchSize}. For EACH batch, create that many subagents in parallel using Task tool (send single message with multiple Task calls). Each subagent analyzes one file and provides insights. After ALL batches complete, consolidate into SINGLE JSON with ONE cohesive PR title/description. Model: ${subagentModel}. Example: 4 files with BATCH_SIZE=1 → 4 sequential batches of 1 subagent each. Example: 4 files with BATCH_SIZE=3 → batch 1 has 3 parallel subagents (files 1-3), batch 2 has 1 subagent (file 4).\n`
|
|
140
|
-
: '';
|
|
141
|
-
|
|
142
|
-
// Truncate full diff if too large
|
|
143
|
-
const truncatedDiff = fullDiff.length > 50000
|
|
144
|
-
? fullDiff.substring(0, 50000) + '\n... (truncated diff)'
|
|
145
|
-
: fullDiff;
|
|
146
50
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
COMMITS: commits,
|
|
152
|
-
DIFF_FILES: diffFiles,
|
|
153
|
-
FULL_DIFF: truncatedDiff
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
info('Sending to Claude for analysis...');
|
|
157
|
-
const startTime = Date.now();
|
|
158
|
-
|
|
159
|
-
// Prepare telemetry context
|
|
160
|
-
const filesChanged = diffFiles.split('\n').length;
|
|
161
|
-
const telemetryContext = {
|
|
162
|
-
fileCount: filesChanged,
|
|
163
|
-
batchSize: filesChanged,
|
|
164
|
-
totalBatches: 1,
|
|
165
|
-
model: subagentModel || 'sonnet',
|
|
166
|
-
hook: 'analyze-diff'
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
// Use cross-platform executeClaudeWithRetry from claude-client.js with telemetry
|
|
171
|
-
const response = await executeClaudeWithRetry(prompt, {
|
|
172
|
-
timeout: 180000, // 3 minutes for diff analysis
|
|
173
|
-
telemetryContext
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
// Extract JSON from response using claude-client utility
|
|
177
|
-
const result = extractJSON(response);
|
|
51
|
+
// Log truncation details if applicable
|
|
52
|
+
if (result.context.isTruncated) {
|
|
53
|
+
logger.debug('analyze-diff', 'Diff was truncated', result.context.truncationDetails);
|
|
54
|
+
}
|
|
178
55
|
|
|
179
56
|
// Show the results
|
|
57
|
+
const contextDescription = `${result.context.currentBranch} vs ${result.context.baseBranch}`;
|
|
58
|
+
const filesChanged = result.context.filesCount;
|
|
59
|
+
|
|
180
60
|
console.log('');
|
|
181
61
|
console.log('════════════════════════════════════════════════════════════════');
|
|
182
62
|
console.log(' DIFFERENCES ANALYSIS ');
|
|
@@ -215,10 +95,15 @@ export async function runAnalyzeDiff(args) {
|
|
|
215
95
|
|
|
216
96
|
// Save the results in a file with context
|
|
217
97
|
const outputData = {
|
|
218
|
-
|
|
98
|
+
prTitle: result.prTitle,
|
|
99
|
+
prDescription: result.prDescription,
|
|
100
|
+
suggestedBranchName: result.suggestedBranchName,
|
|
101
|
+
changeType: result.changeType,
|
|
102
|
+
breakingChanges: result.breakingChanges,
|
|
103
|
+
testingNotes: result.testingNotes,
|
|
219
104
|
context: {
|
|
220
|
-
currentBranch,
|
|
221
|
-
baseBranch,
|
|
105
|
+
currentBranch: result.context.currentBranch,
|
|
106
|
+
baseBranch: result.context.baseBranch,
|
|
222
107
|
contextDescription,
|
|
223
108
|
filesChanged,
|
|
224
109
|
timestamp: new Date().toISOString()
|
|
@@ -243,13 +128,7 @@ export async function runAnalyzeDiff(args) {
|
|
|
243
128
|
|
|
244
129
|
// Contextual suggestions
|
|
245
130
|
console.log('');
|
|
246
|
-
if (
|
|
247
|
-
// Case of local changes without push
|
|
248
|
-
console.log(`💡 ${colors.yellow}To create new branch with these changes:${colors.reset}`);
|
|
249
|
-
console.log(` git checkout -b ${result.suggestedBranchName}`);
|
|
250
|
-
console.log(` git push -u origin ${result.suggestedBranchName}`);
|
|
251
|
-
} else if (currentBranch !== result.suggestedBranchName) {
|
|
252
|
-
// Normal case of comparison between branches
|
|
131
|
+
if (result.context.currentBranch !== result.suggestedBranchName) {
|
|
253
132
|
console.log(`💡 ${colors.yellow}For renaming your current branch:${colors.reset}`);
|
|
254
133
|
console.log(` git branch -m ${result.suggestedBranchName}`);
|
|
255
134
|
}
|
|
@@ -257,6 +136,6 @@ export async function runAnalyzeDiff(args) {
|
|
|
257
136
|
console.log(`💡 ${colors.yellow}Tip:${colors.reset} Use this information to create your PR on GitHub.`);
|
|
258
137
|
|
|
259
138
|
} catch (e) {
|
|
260
|
-
error(
|
|
139
|
+
error(`Error analyzing diff: ${e.message}`);
|
|
261
140
|
}
|
|
262
141
|
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: analyze.js
|
|
3
|
+
* Purpose: On-demand code analysis command (interactive, runs outside git hooks)
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: Git hooks cannot reliably read stdin for interactive prompts
|
|
6
|
+
* (stdin is redirected from /dev/null). This command provides interactive analysis
|
|
7
|
+
* before committing, allowing developers to review all issues and decide whether
|
|
8
|
+
* to proceed or fix them first.
|
|
9
|
+
*
|
|
10
|
+
* Key features:
|
|
11
|
+
* - Runs outside git hook context (stdin works normally)
|
|
12
|
+
* - Analyzes staged, unstaged, or all tracked files
|
|
13
|
+
* - Interactive confirmation with detailed issue view
|
|
14
|
+
* - Generates resolution prompt on abort
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* claude-hooks analyze # Analyze staged files (default)
|
|
18
|
+
* claude-hooks analyze --unstaged # Analyze unstaged changes
|
|
19
|
+
* claude-hooks analyze --all # Analyze all tracked files
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { getStagedFiles, getUnstagedFiles, getAllTrackedFiles, createCommit } from '../utils/git-operations.js';
|
|
23
|
+
import { filterFiles } from '../utils/file-operations.js';
|
|
24
|
+
import {
|
|
25
|
+
buildFilesData,
|
|
26
|
+
runAnalysis,
|
|
27
|
+
hasAnyIssues,
|
|
28
|
+
displayIssueSummary
|
|
29
|
+
} from '../utils/analysis-engine.js';
|
|
30
|
+
import { promptUserConfirmation, promptConfirmation } from '../utils/interactive-ui.js';
|
|
31
|
+
import { generateResolutionPrompt } from '../utils/resolution-prompt.js';
|
|
32
|
+
import { getConfig } from '../config.js';
|
|
33
|
+
import { loadPreset } from '../utils/preset-loader.js';
|
|
34
|
+
import logger from '../utils/logger.js';
|
|
35
|
+
import { error, success, info } from './helpers.js';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Main analyze command
|
|
39
|
+
* Why: Provides interactive analysis before committing
|
|
40
|
+
*
|
|
41
|
+
* @param {Object} options - Command options
|
|
42
|
+
* @param {boolean} options.staged - Analyze staged files (default: true)
|
|
43
|
+
* @param {boolean} options.unstaged - Analyze unstaged files
|
|
44
|
+
* @param {boolean} options.all - Analyze all tracked files
|
|
45
|
+
*/
|
|
46
|
+
export const runAnalyze = async (options = {}) => {
|
|
47
|
+
const { unstaged = false, all = false } = options;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// Load configuration
|
|
51
|
+
const config = await getConfig();
|
|
52
|
+
|
|
53
|
+
// Enable debug mode from config
|
|
54
|
+
if (config.system?.debug) {
|
|
55
|
+
logger.setDebugMode(true);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Load active preset for file extensions
|
|
59
|
+
const presetName = config.preset || 'default';
|
|
60
|
+
const { metadata } = await loadPreset(presetName);
|
|
61
|
+
const allowedExtensions = metadata.fileExtensions;
|
|
62
|
+
|
|
63
|
+
// Determine scope
|
|
64
|
+
let scopeLabel = 'staged changes';
|
|
65
|
+
if (all) {
|
|
66
|
+
scopeLabel = 'all tracked files';
|
|
67
|
+
} else if (unstaged) {
|
|
68
|
+
scopeLabel = 'unstaged changes';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
info(`Analyzing ${scopeLabel} with '${metadata.displayName}' preset...`);
|
|
72
|
+
|
|
73
|
+
// Get files based on scope
|
|
74
|
+
let files = [];
|
|
75
|
+
if (all) {
|
|
76
|
+
files = getAllTrackedFiles({ extensions: allowedExtensions });
|
|
77
|
+
} else if (unstaged) {
|
|
78
|
+
files = getUnstagedFiles({ extensions: allowedExtensions });
|
|
79
|
+
} else {
|
|
80
|
+
files = getStagedFiles({ extensions: allowedExtensions });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (files.length === 0) {
|
|
84
|
+
info(`No files to analyze in ${scopeLabel}.`);
|
|
85
|
+
process.exit(0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
logger.debug('analyze', 'Files found', {
|
|
89
|
+
scope: scopeLabel,
|
|
90
|
+
count: files.length,
|
|
91
|
+
extensions: allowedExtensions
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Filter files by size
|
|
95
|
+
const filteredFiles = await filterFiles(files, {
|
|
96
|
+
maxSize: config.analysis?.maxFileSize || 1048576,
|
|
97
|
+
extensions: allowedExtensions
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const validFiles = filteredFiles.filter(f => f.valid);
|
|
101
|
+
const invalidFiles = filteredFiles.filter(f => !f.valid);
|
|
102
|
+
|
|
103
|
+
// Show warnings for skipped files
|
|
104
|
+
if (invalidFiles.length > 0) {
|
|
105
|
+
invalidFiles.forEach(file => {
|
|
106
|
+
logger.warning(`Skipping ${file.path}: ${file.reason}`);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (validFiles.length === 0) {
|
|
111
|
+
info(`No valid files found to analyze in ${scopeLabel}.`);
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
info(`Sending ${validFiles.length} file(s) for analysis...`);
|
|
116
|
+
|
|
117
|
+
// Build file data (diff/content) using shared engine
|
|
118
|
+
const filesData = buildFilesData(validFiles, { staged: !unstaged && !all });
|
|
119
|
+
|
|
120
|
+
if (filesData.length === 0) {
|
|
121
|
+
info('No file data could be extracted.');
|
|
122
|
+
process.exit(0);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Run analysis using shared engine
|
|
126
|
+
const result = await runAnalysis(filesData, config, { hook: 'analyze' });
|
|
127
|
+
|
|
128
|
+
// Check results
|
|
129
|
+
if (!hasAnyIssues(result)) {
|
|
130
|
+
console.log('');
|
|
131
|
+
success('No issues found. Code is ready to commit!');
|
|
132
|
+
console.log('');
|
|
133
|
+
|
|
134
|
+
// Prompt user to commit or cancel
|
|
135
|
+
const shouldCommit = await promptConfirmation('Create commit now?', true);
|
|
136
|
+
|
|
137
|
+
if (shouldCommit) {
|
|
138
|
+
info('Creating commit with auto-generated message...');
|
|
139
|
+
console.log('');
|
|
140
|
+
|
|
141
|
+
const commitResult = createCommit('auto', { noVerify: true });
|
|
142
|
+
|
|
143
|
+
if (commitResult.success) {
|
|
144
|
+
success('Commit created successfully!');
|
|
145
|
+
if (commitResult.output) {
|
|
146
|
+
console.log(commitResult.output);
|
|
147
|
+
}
|
|
148
|
+
console.log('');
|
|
149
|
+
process.exit(0);
|
|
150
|
+
} else {
|
|
151
|
+
error(`Commit failed: ${commitResult.error}`);
|
|
152
|
+
console.log('');
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
info('Commit cancelled. Staged files remain unchanged.');
|
|
157
|
+
console.log('');
|
|
158
|
+
process.exit(0);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Display summary
|
|
163
|
+
console.log('');
|
|
164
|
+
console.log('Analysis complete:');
|
|
165
|
+
displayIssueSummary(result);
|
|
166
|
+
console.log('');
|
|
167
|
+
|
|
168
|
+
// Interactive confirmation (works outside git hook)
|
|
169
|
+
const userChoice = await promptUserConfirmation(result);
|
|
170
|
+
|
|
171
|
+
if (userChoice === 'abort') {
|
|
172
|
+
// Generate resolution prompt
|
|
173
|
+
await generateResolutionPrompt(result, {
|
|
174
|
+
fileCount: filesData.length
|
|
175
|
+
});
|
|
176
|
+
console.log('');
|
|
177
|
+
success('Resolution prompt generated: claude_resolution_prompt.md');
|
|
178
|
+
info('Fix issues and run `claude-hooks analyze` again.');
|
|
179
|
+
console.log('');
|
|
180
|
+
process.exit(1);
|
|
181
|
+
} else {
|
|
182
|
+
// User chose to continue - execute commit automatically
|
|
183
|
+
console.log('');
|
|
184
|
+
|
|
185
|
+
// Safeguard: verify staged files still exist
|
|
186
|
+
const currentStagedFiles = getStagedFiles({ extensions: allowedExtensions });
|
|
187
|
+
if (currentStagedFiles.length === 0) {
|
|
188
|
+
error('No staged files found. Did you unstage changes?');
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
info('Creating commit with auto-generated message...');
|
|
193
|
+
console.log('');
|
|
194
|
+
|
|
195
|
+
// Execute commit with --no-verify (skip hooks - we already analyzed)
|
|
196
|
+
const commitResult = createCommit('auto', { noVerify: true });
|
|
197
|
+
|
|
198
|
+
if (commitResult.success) {
|
|
199
|
+
success('Commit created successfully!');
|
|
200
|
+
if (commitResult.output) {
|
|
201
|
+
console.log(commitResult.output);
|
|
202
|
+
}
|
|
203
|
+
console.log('');
|
|
204
|
+
process.exit(0);
|
|
205
|
+
} else {
|
|
206
|
+
error(`Commit failed: ${commitResult.error}`);
|
|
207
|
+
console.log('');
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
} catch (err) {
|
|
213
|
+
logger.error('analyze', 'Analysis failed', err);
|
|
214
|
+
error(`Analysis failed: ${err.message}`);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
};
|