claude-git-hooks 2.35.3 → 2.43.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.
- package/CHANGELOG.md +135 -0
- package/CLAUDE.md +24 -1389
- package/README.md +113 -0
- package/bin/claude-hooks +11 -7
- package/lib/cli-metadata.js +17 -3
- package/lib/commands/analyze-pr.js +270 -145
- package/lib/commands/analyze.js +151 -3
- package/lib/commands/create-pr.js +345 -134
- package/lib/commands/helpers.js +9 -4
- package/lib/commands/hooks.js +5 -5
- package/lib/commands/install.js +77 -28
- package/lib/commands/lint.js +120 -4
- package/lib/config.js +3 -0
- package/lib/hooks/pre-commit.js +26 -5
- package/lib/hooks/prepare-commit-msg.js +78 -4
- package/lib/utils/analysis-engine.js +12 -6
- package/lib/utils/claude-client.js +222 -12
- package/lib/utils/claude-diagnostics.js +5 -4
- package/lib/utils/cost-tracker.js +128 -0
- package/lib/utils/diff-analysis-orchestrator.js +2 -1
- package/lib/utils/git-operations.js +105 -2
- package/lib/utils/hooks-verified-marker.js +121 -0
- package/lib/utils/interactive-ui.js +4 -4
- package/lib/utils/judge.js +3 -2
- package/lib/utils/langfuse-tracer.js +156 -0
- package/lib/utils/logger.js +30 -5
- package/lib/utils/pr-metadata-engine.js +4 -2
- package/package.json +4 -2
package/lib/commands/analyze.js
CHANGED
|
@@ -12,19 +12,25 @@
|
|
|
12
12
|
* - Analyzes staged, unstaged, or all tracked files
|
|
13
13
|
* - Interactive confirmation with detailed issue view
|
|
14
14
|
* - Generates resolution prompt on abort
|
|
15
|
+
* - Headless mode for CI (--headless): skips prompts, never commits
|
|
16
|
+
* - JSON output (--format json): single JSON line on stdout
|
|
15
17
|
*
|
|
16
18
|
* Usage:
|
|
17
19
|
* claude-hooks analyze # Analyze staged files (default)
|
|
18
20
|
* claude-hooks analyze --unstaged # Analyze unstaged changes
|
|
19
21
|
* claude-hooks analyze --all # Analyze all tracked files
|
|
22
|
+
* claude-hooks analyze --headless --format json # CI mode with JSON output
|
|
20
23
|
*/
|
|
21
24
|
|
|
22
25
|
import {
|
|
23
26
|
getStagedFiles,
|
|
24
27
|
getUnstagedFiles,
|
|
25
28
|
getAllTrackedFiles,
|
|
26
|
-
createCommit
|
|
29
|
+
createCommit,
|
|
30
|
+
getRepoRoot,
|
|
31
|
+
getStagedTreeSha
|
|
27
32
|
} from '../utils/git-operations.js';
|
|
33
|
+
import { writeMarker } from '../utils/hooks-verified-marker.js';
|
|
28
34
|
import { filterFiles } from '../utils/file-operations.js';
|
|
29
35
|
import {
|
|
30
36
|
buildFilesData,
|
|
@@ -36,9 +42,52 @@ import { promptUserConfirmation, promptConfirmation } from '../utils/interactive
|
|
|
36
42
|
import { generateResolutionPrompt } from '../utils/resolution-prompt.js';
|
|
37
43
|
import { getConfig } from '../config.js';
|
|
38
44
|
import { loadPreset } from '../utils/preset-loader.js';
|
|
45
|
+
import { CostTracker } from '../utils/cost-tracker.js';
|
|
46
|
+
import { getVersion } from '../utils/package-info.js';
|
|
39
47
|
import logger from '../utils/logger.js';
|
|
40
48
|
import { error, success, info } from './helpers.js';
|
|
41
49
|
|
|
50
|
+
// ─── JSON Error Helper ────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Emit error JSON to stdout and set exit code
|
|
54
|
+
* @param {string} message - Error message
|
|
55
|
+
* @param {number} exitCode - Process exit code (default: 1)
|
|
56
|
+
* @private
|
|
57
|
+
*/
|
|
58
|
+
function _emitErrorJSON(message, exitCode = 1) {
|
|
59
|
+
process.stdout.write(`${JSON.stringify({ status: 'error', error: message })}\n`);
|
|
60
|
+
process.exitCode = exitCode;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Emit JSON result to stdout (does not exit — caller handles exit)
|
|
65
|
+
* @param {Object} payload - JSON payload
|
|
66
|
+
* @private
|
|
67
|
+
*/
|
|
68
|
+
function _emitJSON(payload) {
|
|
69
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Build no-files JSON payload for headless/JSON mode
|
|
74
|
+
* @private
|
|
75
|
+
*/
|
|
76
|
+
function _buildNoFilesPayload(scope, presetName, startTime, skippedCount, costTracker) {
|
|
77
|
+
return {
|
|
78
|
+
status: 'no-files',
|
|
79
|
+
scope,
|
|
80
|
+
preset: presetName,
|
|
81
|
+
fileCount: 0,
|
|
82
|
+
skippedCount,
|
|
83
|
+
durationMs: Date.now() - startTime,
|
|
84
|
+
issueCount: 0,
|
|
85
|
+
severityCounts: { blocker: 0, critical: 0, major: 0, minor: 0, info: 0 },
|
|
86
|
+
issues: [],
|
|
87
|
+
costs: costTracker ? costTracker.toJSON() : null
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
42
91
|
/**
|
|
43
92
|
* Main analyze command
|
|
44
93
|
* Why: Provides interactive analysis before committing
|
|
@@ -47,9 +96,24 @@ import { error, success, info } from './helpers.js';
|
|
|
47
96
|
* @param {boolean} options.staged - Analyze staged files (default: true)
|
|
48
97
|
* @param {boolean} options.unstaged - Analyze unstaged files
|
|
49
98
|
* @param {boolean} options.all - Analyze all tracked files
|
|
99
|
+
* @param {boolean} options.headless - Skip interactive prompts (CI mode)
|
|
100
|
+
* @param {string} options.format - Output format ('json' or null)
|
|
50
101
|
*/
|
|
51
102
|
export const runAnalyze = async (options = {}) => {
|
|
52
|
-
const { unstaged = false, all = false } = options;
|
|
103
|
+
const { unstaged = false, all = false, headless = false, format = null } = options;
|
|
104
|
+
const isJSON = format === 'json';
|
|
105
|
+
|
|
106
|
+
// Disallowed combo: --format json without --headless
|
|
107
|
+
if (isJSON && !headless) {
|
|
108
|
+
_emitErrorJSON('--format json requires --headless', 2);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Activate JSON mode before any output so info/warning route to stderr
|
|
113
|
+
if (isJSON) logger.setJSONMode(true);
|
|
114
|
+
|
|
115
|
+
const startTime = Date.now();
|
|
116
|
+
const costTracker = headless ? new CostTracker() : null;
|
|
53
117
|
|
|
54
118
|
try {
|
|
55
119
|
// Load configuration
|
|
@@ -66,6 +130,7 @@ export const runAnalyze = async (options = {}) => {
|
|
|
66
130
|
const allowedExtensions = metadata.fileExtensions;
|
|
67
131
|
|
|
68
132
|
// Determine scope
|
|
133
|
+
const scope = all ? 'all' : unstaged ? 'unstaged' : 'staged';
|
|
69
134
|
let scopeLabel = 'staged changes';
|
|
70
135
|
if (all) {
|
|
71
136
|
scopeLabel = 'all tracked files';
|
|
@@ -86,8 +151,12 @@ export const runAnalyze = async (options = {}) => {
|
|
|
86
151
|
}
|
|
87
152
|
|
|
88
153
|
if (files.length === 0) {
|
|
154
|
+
if (isJSON) {
|
|
155
|
+
_emitJSON(_buildNoFilesPayload(scope, presetName, startTime, 0, costTracker));
|
|
156
|
+
}
|
|
89
157
|
info(`No files to analyze in ${scopeLabel}.`);
|
|
90
158
|
process.exit(0);
|
|
159
|
+
return; // guard test-mode fallthrough
|
|
91
160
|
}
|
|
92
161
|
|
|
93
162
|
logger.debug('analyze', 'Files found', {
|
|
@@ -113,8 +182,12 @@ export const runAnalyze = async (options = {}) => {
|
|
|
113
182
|
}
|
|
114
183
|
|
|
115
184
|
if (validFiles.length === 0) {
|
|
185
|
+
if (isJSON) {
|
|
186
|
+
_emitJSON(_buildNoFilesPayload(scope, presetName, startTime, invalidFiles.length, costTracker));
|
|
187
|
+
}
|
|
116
188
|
info(`No valid files found to analyze in ${scopeLabel}.`);
|
|
117
189
|
process.exit(0);
|
|
190
|
+
return; // guard test-mode fallthrough
|
|
118
191
|
}
|
|
119
192
|
|
|
120
193
|
info(`Sending ${validFiles.length} file(s) for analysis...`);
|
|
@@ -123,12 +196,83 @@ export const runAnalyze = async (options = {}) => {
|
|
|
123
196
|
const filesData = buildFilesData(validFiles, { staged: !unstaged && !all });
|
|
124
197
|
|
|
125
198
|
if (filesData.length === 0) {
|
|
199
|
+
if (isJSON) {
|
|
200
|
+
_emitJSON({
|
|
201
|
+
status: 'no-files',
|
|
202
|
+
scope,
|
|
203
|
+
preset: presetName,
|
|
204
|
+
fileCount: 0,
|
|
205
|
+
skippedCount: invalidFiles.length,
|
|
206
|
+
durationMs: Date.now() - startTime,
|
|
207
|
+
issueCount: 0,
|
|
208
|
+
severityCounts: { blocker: 0, critical: 0, major: 0, minor: 0, info: 0 },
|
|
209
|
+
issues: [],
|
|
210
|
+
costs: costTracker ? costTracker.toJSON() : null
|
|
211
|
+
});
|
|
212
|
+
}
|
|
126
213
|
info('No file data could be extracted.');
|
|
127
214
|
process.exit(0);
|
|
215
|
+
return;
|
|
128
216
|
}
|
|
129
217
|
|
|
130
218
|
// Run analysis using shared engine
|
|
131
|
-
const result = await runAnalysis(filesData, config, {
|
|
219
|
+
const result = await runAnalysis(filesData, config, {
|
|
220
|
+
hook: 'analyze',
|
|
221
|
+
headless,
|
|
222
|
+
costTracker
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Write hooks-verified marker when analysis completes without critical/blocker issues.
|
|
226
|
+
// For headless: marker stays for a downstream `git commit` to pick up.
|
|
227
|
+
// For interactive: prepare-commit-msg reads it when createCommit() fires.
|
|
228
|
+
const { blocker = 0, critical = 0 } = result.issues || {};
|
|
229
|
+
const hasCritical = (blocker + critical) > 0;
|
|
230
|
+
|
|
231
|
+
if (!hasCritical && scope === 'staged') {
|
|
232
|
+
try {
|
|
233
|
+
const repoRoot = getRepoRoot();
|
|
234
|
+
const treeSha = getStagedTreeSha();
|
|
235
|
+
const version = await getVersion();
|
|
236
|
+
writeMarker(repoRoot, treeSha, version);
|
|
237
|
+
logger.debug('analyze', 'Hooks-verified marker written', { treeSha });
|
|
238
|
+
} catch (markerErr) {
|
|
239
|
+
logger.warning(`Could not write hooks-verified marker: ${markerErr.message}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ── Headless path: emit result and exit (never commit) ──
|
|
244
|
+
if (headless) {
|
|
245
|
+
const { major = 0, minor = 0, info: infoCount = 0 } = result.issues || {};
|
|
246
|
+
|
|
247
|
+
if (isJSON) {
|
|
248
|
+
_emitJSON({
|
|
249
|
+
status: hasCritical ? 'fail' : 'pass',
|
|
250
|
+
scope,
|
|
251
|
+
preset: presetName,
|
|
252
|
+
fileCount: validFiles.length,
|
|
253
|
+
skippedCount: invalidFiles.length,
|
|
254
|
+
durationMs: Date.now() - startTime,
|
|
255
|
+
issueCount: blocker + critical + major + minor + infoCount,
|
|
256
|
+
severityCounts: { blocker, critical, major, minor, info: infoCount },
|
|
257
|
+
issues: result.details || [],
|
|
258
|
+
costs: costTracker ? costTracker.toJSON() : null
|
|
259
|
+
});
|
|
260
|
+
process.exit(hasCritical ? 1 : 0);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Headless without JSON: print summary to stderr, exit
|
|
265
|
+
if (hasAnyIssues(result)) {
|
|
266
|
+
displayIssueSummary(result);
|
|
267
|
+
} else {
|
|
268
|
+
success('No issues found. Code is ready to commit!');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
process.exit(hasCritical ? 1 : 0);
|
|
272
|
+
return; // guard test-mode fallthrough
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ── Interactive path (unchanged) ──
|
|
132
276
|
|
|
133
277
|
// Check results
|
|
134
278
|
if (!hasAnyIssues(result)) {
|
|
@@ -215,6 +359,10 @@ export const runAnalyze = async (options = {}) => {
|
|
|
215
359
|
}
|
|
216
360
|
} catch (err) {
|
|
217
361
|
logger.error('analyze', 'Analysis failed', err);
|
|
362
|
+
if (isJSON) {
|
|
363
|
+
_emitErrorJSON(`Analysis failed: ${err.message}`);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
218
366
|
error(`Analysis failed: ${err.message}`);
|
|
219
367
|
process.exit(1);
|
|
220
368
|
}
|