claude-git-hooks 2.33.1 → 2.34.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 +19 -0
- package/CLAUDE.md +53 -12
- package/bin/claude-hooks +1 -0
- package/lib/cli-metadata.js +9 -0
- package/lib/commands/install.js +24 -0
- package/lib/commands/lint.js +187 -0
- package/lib/config.js +7 -0
- package/lib/hooks/pre-commit.js +97 -31
- package/lib/utils/judge.js +11 -9
- package/lib/utils/linter-runner.js +443 -0
- package/lib/utils/tool-runner.js +418 -0
- package/package.json +69 -69
- package/templates/config.advanced.example.json +38 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: tool-runner.js
|
|
3
|
+
* Purpose: Generic external tool executor for pre-commit pipeline
|
|
4
|
+
*
|
|
5
|
+
* Shared infrastructure for running external tools (linters, formatters)
|
|
6
|
+
* on staged or user-specified files. Each tool is defined as a ToolDefinition
|
|
7
|
+
* object with command, args builders, output parser, and install hints.
|
|
8
|
+
*
|
|
9
|
+
* Designed for extensibility:
|
|
10
|
+
* - Linters use this via linter-runner.js (Issue #22)
|
|
11
|
+
* - Formatters will use this via formatter-runner.js (Issue #26)
|
|
12
|
+
*
|
|
13
|
+
* Dependencies:
|
|
14
|
+
* - which-command: Cross-platform executable resolution
|
|
15
|
+
* - child_process: Tool execution
|
|
16
|
+
* - git-operations: Re-staging fixed files
|
|
17
|
+
* - logger: Debug and error logging
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { execSync } from 'child_process';
|
|
21
|
+
import fs from 'fs';
|
|
22
|
+
import path from 'path';
|
|
23
|
+
import { which } from './which-command.js';
|
|
24
|
+
import { walkDirectoryTree } from './file-utils.js';
|
|
25
|
+
import logger from './logger.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {Object} ToolDefinition
|
|
29
|
+
* @property {string} name - Tool display name (e.g., 'eslint')
|
|
30
|
+
* @property {string} command - Primary command to execute (e.g., 'npx', 'mvn')
|
|
31
|
+
* @property {function(string[]): string[]} args - Build args for check mode
|
|
32
|
+
* @property {function(string[]): string[]} [fixArgs] - Build args for auto-fix mode
|
|
33
|
+
* @property {string} detectCommand - Binary to check via which() (e.g., 'eslint')
|
|
34
|
+
* @property {string} [detectFallback] - Fallback path to check (e.g., 'node_modules/.bin/eslint')
|
|
35
|
+
* @property {string} installHint - Install instruction shown when tool is missing
|
|
36
|
+
* @property {string[]} extensions - File extensions this tool handles
|
|
37
|
+
* @property {function(string): Object} parseOutput - Parse stdout into structured results
|
|
38
|
+
* @property {number} [timeout] - Per-tool timeout in ms (default from config)
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {Object} ToolIssue
|
|
43
|
+
* @property {string} file - File path
|
|
44
|
+
* @property {number} [line] - Line number
|
|
45
|
+
* @property {number} [column] - Column number
|
|
46
|
+
* @property {string} severity - 'error' or 'warning'
|
|
47
|
+
* @property {string} message - Issue description
|
|
48
|
+
* @property {string} [ruleId] - Linter rule identifier
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @typedef {Object} ToolRunResult
|
|
53
|
+
* @property {string} tool - Tool name
|
|
54
|
+
* @property {boolean} skipped - Whether tool was skipped (not installed)
|
|
55
|
+
* @property {string} [skipReason] - Why the tool was skipped
|
|
56
|
+
* @property {ToolIssue[]} errors - Error-level issues
|
|
57
|
+
* @property {ToolIssue[]} warnings - Warning-level issues
|
|
58
|
+
* @property {number} fixedCount - Number of issues auto-fixed
|
|
59
|
+
* @property {string[]} fixedFiles - Files that were auto-fixed
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if a tool is available on the system
|
|
64
|
+
*
|
|
65
|
+
* @param {ToolDefinition} toolDef - Tool definition
|
|
66
|
+
* @returns {{ available: boolean, path: string|null }} Availability info
|
|
67
|
+
*/
|
|
68
|
+
export function isToolAvailable(toolDef) {
|
|
69
|
+
// Strategy 1: Check detectCommand in PATH (global install)
|
|
70
|
+
const resolved = which(toolDef.detectCommand);
|
|
71
|
+
if (resolved) {
|
|
72
|
+
logger.debug('tool-runner - isToolAvailable', `Found ${toolDef.name} in PATH`, { path: resolved });
|
|
73
|
+
return { available: true, path: resolved };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Strategy 2: Check project files for the tool as a dependency
|
|
77
|
+
// Why: local installs (npm install --save-dev) live in node_modules/.bin,
|
|
78
|
+
// not in PATH. npx resolves them at runtime, so we just need to confirm
|
|
79
|
+
// the tool is declared in a project file (package.json deps or pom.xml plugin).
|
|
80
|
+
// Uses walkDirectoryTree (same walker as version-manager) to scan up to 3 levels.
|
|
81
|
+
if (toolDef.detectInProjectFile) {
|
|
82
|
+
const found = _detectInProjectFiles(toolDef);
|
|
83
|
+
if (found) {
|
|
84
|
+
// Tool is configured but the command binary is missing
|
|
85
|
+
if (found.configured && !found.available) {
|
|
86
|
+
logger.debug('tool-runner - isToolAvailable',
|
|
87
|
+
`${toolDef.name} configured but '${found.missingCommand}' not in PATH`
|
|
88
|
+
);
|
|
89
|
+
return {
|
|
90
|
+
available: false,
|
|
91
|
+
path: null,
|
|
92
|
+
installHint: `${toolDef.name} is configured in your project but '${found.missingCommand}' is not in PATH. Install it and ensure it's accessible from your terminal.`
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return found;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
logger.debug('tool-runner - isToolAvailable', `${toolDef.name} not found`);
|
|
100
|
+
return { available: false, path: null };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Scan project files (package.json, pom.xml) up to 3 levels deep
|
|
105
|
+
* to detect if a tool is declared as a dependency or plugin.
|
|
106
|
+
*
|
|
107
|
+
* @param {ToolDefinition} toolDef - Tool definition with detectInProjectFile
|
|
108
|
+
* @returns {{ available: boolean, path: string|null } | null} Result or null if not found
|
|
109
|
+
*/
|
|
110
|
+
export function _detectInProjectFiles(toolDef) {
|
|
111
|
+
const { filename, check } = toolDef.detectInProjectFile;
|
|
112
|
+
let found = false;
|
|
113
|
+
|
|
114
|
+
walkDirectoryTree(process.cwd(), {
|
|
115
|
+
maxDepth: 3,
|
|
116
|
+
ignoreSet: new Set([
|
|
117
|
+
'.git', 'node_modules', 'target', 'build', 'dist',
|
|
118
|
+
'__pycache__', '.venv', 'vendor', 'out', 'coverage'
|
|
119
|
+
]),
|
|
120
|
+
onFile: (entry, fullPath) => {
|
|
121
|
+
if (found) return; // short-circuit after first match
|
|
122
|
+
if (entry.name === filename) {
|
|
123
|
+
try {
|
|
124
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
125
|
+
if (check(content)) {
|
|
126
|
+
logger.debug('tool-runner - _detectInProjectFiles',
|
|
127
|
+
`Found ${toolDef.name} in ${path.relative(process.cwd(), fullPath)}`
|
|
128
|
+
);
|
|
129
|
+
found = true;
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
// File read error — skip
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (!found) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Project file declares the tool, but can we actually run the command?
|
|
143
|
+
// npx-based tools (eslint, etc.) are resolved by npx at runtime — always OK.
|
|
144
|
+
// Non-npx tools (mvn, sqlfluff, etc.) need the command binary in PATH.
|
|
145
|
+
if (toolDef.command !== 'npx') {
|
|
146
|
+
const cmdResolved = which(toolDef.command);
|
|
147
|
+
if (!cmdResolved) {
|
|
148
|
+
logger.debug('tool-runner - _detectInProjectFiles',
|
|
149
|
+
`${toolDef.name} configured in project but '${toolDef.command}' not in PATH`
|
|
150
|
+
);
|
|
151
|
+
return {
|
|
152
|
+
available: false,
|
|
153
|
+
path: null,
|
|
154
|
+
configured: true,
|
|
155
|
+
missingCommand: toolDef.command
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { available: true, path: toolDef.command };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Filter files by tool's supported extensions
|
|
165
|
+
*
|
|
166
|
+
* @param {string[]} files - File paths
|
|
167
|
+
* @param {ToolDefinition} toolDef - Tool definition
|
|
168
|
+
* @returns {string[]} Files matching tool's extensions
|
|
169
|
+
*/
|
|
170
|
+
export function filterFilesByTool(files, toolDef) {
|
|
171
|
+
return files.filter((file) => {
|
|
172
|
+
const ext = path.extname(file).toLowerCase();
|
|
173
|
+
return toolDef.extensions.includes(ext);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Execute a tool on files and parse output
|
|
179
|
+
*
|
|
180
|
+
* @param {ToolDefinition} toolDef - Tool definition
|
|
181
|
+
* @param {string[]} files - Files to process
|
|
182
|
+
* @param {Object} options - Execution options
|
|
183
|
+
* @param {number} [options.timeout] - Timeout in ms
|
|
184
|
+
* @param {string} [options.cwd] - Working directory
|
|
185
|
+
* @returns {ToolRunResult} Parsed result
|
|
186
|
+
*/
|
|
187
|
+
export function runTool(toolDef, files, options = {}) {
|
|
188
|
+
const { timeout = toolDef.timeout || 30000, cwd = process.cwd() } = options;
|
|
189
|
+
|
|
190
|
+
const args = toolDef.args(files);
|
|
191
|
+
const fullCommand = [toolDef.command, ...args].join(' ');
|
|
192
|
+
|
|
193
|
+
logger.debug('tool-runner - runTool', `Executing ${toolDef.name}`, {
|
|
194
|
+
command: fullCommand,
|
|
195
|
+
fileCount: files.length,
|
|
196
|
+
timeout
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const result = {
|
|
200
|
+
tool: toolDef.name,
|
|
201
|
+
skipped: false,
|
|
202
|
+
errors: [],
|
|
203
|
+
warnings: [],
|
|
204
|
+
fixedCount: 0,
|
|
205
|
+
fixedFiles: []
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const stdout = execSync(fullCommand, {
|
|
210
|
+
encoding: 'utf8',
|
|
211
|
+
timeout,
|
|
212
|
+
cwd,
|
|
213
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Exit code 0 = no issues; parse anyway for warnings
|
|
217
|
+
if (toolDef.parseOutput && stdout.trim()) {
|
|
218
|
+
const parsed = toolDef.parseOutput(stdout);
|
|
219
|
+
result.errors = parsed.errors || [];
|
|
220
|
+
result.warnings = parsed.warnings || [];
|
|
221
|
+
}
|
|
222
|
+
} catch (err) {
|
|
223
|
+
// Many linters exit non-zero when issues are found — this is expected
|
|
224
|
+
if (err.stdout && toolDef.parseOutput) {
|
|
225
|
+
const parsed = toolDef.parseOutput(err.stdout);
|
|
226
|
+
result.errors = parsed.errors || [];
|
|
227
|
+
result.warnings = parsed.warnings || [];
|
|
228
|
+
} else if (err.killed) {
|
|
229
|
+
// Timeout
|
|
230
|
+
logger.warning(`${toolDef.name} timed out after ${timeout}ms`);
|
|
231
|
+
result.errors.push({
|
|
232
|
+
file: '',
|
|
233
|
+
severity: 'error',
|
|
234
|
+
message: `${toolDef.name} timed out after ${timeout / 1000}s`
|
|
235
|
+
});
|
|
236
|
+
} else {
|
|
237
|
+
// Genuine execution error
|
|
238
|
+
const stderr = err.stderr ? err.stderr.toString().trim() : '';
|
|
239
|
+
logger.debug('tool-runner - runTool', `${toolDef.name} execution error`, {
|
|
240
|
+
exitCode: err.status,
|
|
241
|
+
stderr
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// If no parseable output, treat as tool error
|
|
245
|
+
if (!err.stdout) {
|
|
246
|
+
result.errors.push({
|
|
247
|
+
file: '',
|
|
248
|
+
severity: 'error',
|
|
249
|
+
message: `${toolDef.name} failed: ${stderr || err.message}`
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
logger.debug('tool-runner - runTool', `${toolDef.name} complete`, {
|
|
256
|
+
errors: result.errors.length,
|
|
257
|
+
warnings: result.warnings.length
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Run a tool's auto-fix command, then re-stage fixed files
|
|
265
|
+
*
|
|
266
|
+
* @param {ToolDefinition} toolDef - Tool definition (must have fixArgs)
|
|
267
|
+
* @param {string[]} files - Files to fix
|
|
268
|
+
* @param {Object} options - Execution options
|
|
269
|
+
* @param {number} [options.timeout] - Timeout in ms
|
|
270
|
+
* @param {string} [options.cwd] - Working directory
|
|
271
|
+
* @param {boolean} [options.restage] - Re-stage fixed files (default: true)
|
|
272
|
+
* @returns {{ fixedFiles: string[], fixedCount: number }} Fix results
|
|
273
|
+
*/
|
|
274
|
+
export function runToolFix(toolDef, files, options = {}) {
|
|
275
|
+
const { timeout = toolDef.timeout || 30000, cwd = process.cwd(), restage = true } = options;
|
|
276
|
+
|
|
277
|
+
if (!toolDef.fixArgs) {
|
|
278
|
+
logger.debug('tool-runner - runToolFix', `${toolDef.name} has no fixArgs, skipping`);
|
|
279
|
+
return { fixedFiles: [], fixedCount: 0 };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const args = toolDef.fixArgs(files);
|
|
283
|
+
const fullCommand = [toolDef.command, ...args].join(' ');
|
|
284
|
+
|
|
285
|
+
logger.debug('tool-runner - runToolFix', `Auto-fixing with ${toolDef.name}`, {
|
|
286
|
+
command: fullCommand,
|
|
287
|
+
fileCount: files.length
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
execSync(fullCommand, {
|
|
292
|
+
encoding: 'utf8',
|
|
293
|
+
timeout,
|
|
294
|
+
cwd,
|
|
295
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
296
|
+
});
|
|
297
|
+
} catch (err) {
|
|
298
|
+
if (err.killed) {
|
|
299
|
+
logger.warning(`${toolDef.name} auto-fix timed out`);
|
|
300
|
+
return { fixedFiles: [], fixedCount: 0 };
|
|
301
|
+
}
|
|
302
|
+
// Distinguish "command not found" from "partial fix exited non-zero"
|
|
303
|
+
// Exit code 1 with stderr containing "not recognized" or "not found" = command missing
|
|
304
|
+
const stderr = err.stderr ? err.stderr.toString() : '';
|
|
305
|
+
if (err.status === 1 && !err.stdout && (
|
|
306
|
+
stderr.includes('not recognized') ||
|
|
307
|
+
stderr.includes('not found') ||
|
|
308
|
+
stderr.includes('ENOENT')
|
|
309
|
+
)) {
|
|
310
|
+
logger.debug('tool-runner - runToolFix', `${toolDef.name} command not available`, {
|
|
311
|
+
stderr: stderr.trim()
|
|
312
|
+
});
|
|
313
|
+
return { fixedFiles: [], fixedCount: 0 };
|
|
314
|
+
}
|
|
315
|
+
// Genuine partial fix — proceed to restage
|
|
316
|
+
logger.debug('tool-runner - runToolFix', `${toolDef.name} fix exited non-zero`, {
|
|
317
|
+
exitCode: err.status
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Re-stage the files that were modified by the fix
|
|
322
|
+
const fixedFiles = [];
|
|
323
|
+
if (restage) {
|
|
324
|
+
for (const file of files) {
|
|
325
|
+
try {
|
|
326
|
+
execSync(`git add "${file}"`, { cwd, stdio: 'pipe' });
|
|
327
|
+
fixedFiles.push(file);
|
|
328
|
+
} catch {
|
|
329
|
+
// File may not have changed; that's fine
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
logger.debug('tool-runner - runToolFix', `${toolDef.name} fix complete`, {
|
|
335
|
+
restagedFiles: fixedFiles.length
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
return { fixedFiles, fixedCount: fixedFiles.length };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Run a tool with auto-fix: check → fix → re-check
|
|
343
|
+
*
|
|
344
|
+
* @param {ToolDefinition} toolDef - Tool definition
|
|
345
|
+
* @param {string[]} files - Files to process
|
|
346
|
+
* @param {Object} options - Execution options
|
|
347
|
+
* @param {boolean} [options.autoFix] - Enable auto-fix (default: false)
|
|
348
|
+
* @param {number} [options.timeout] - Timeout in ms
|
|
349
|
+
* @param {string} [options.cwd] - Working directory
|
|
350
|
+
* @param {boolean} [options.restage] - Re-stage after fix (default: true)
|
|
351
|
+
* @returns {ToolRunResult} Final result after optional fix
|
|
352
|
+
*/
|
|
353
|
+
export function runToolWithAutoFix(toolDef, files, options = {}) {
|
|
354
|
+
const { autoFix = false } = options;
|
|
355
|
+
|
|
356
|
+
// Step 1: Initial check
|
|
357
|
+
const initialResult = runTool(toolDef, files, options);
|
|
358
|
+
|
|
359
|
+
// Step 2: If any issues (errors or warnings) and autoFix enabled, try to fix
|
|
360
|
+
const hasIssues = initialResult.errors.length > 0 || initialResult.warnings.length > 0;
|
|
361
|
+
if (autoFix && toolDef.fixArgs && hasIssues) {
|
|
362
|
+
logger.info(`🔧 Auto-fixing ${toolDef.name} issues...`);
|
|
363
|
+
|
|
364
|
+
const fixResult = runToolFix(toolDef, files, options);
|
|
365
|
+
|
|
366
|
+
if (fixResult.fixedCount > 0) {
|
|
367
|
+
// Step 3: Re-check after fix
|
|
368
|
+
const recheck = runTool(toolDef, files, options);
|
|
369
|
+
recheck.fixedCount = fixResult.fixedCount;
|
|
370
|
+
recheck.fixedFiles = fixResult.fixedFiles;
|
|
371
|
+
return recheck;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return initialResult;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Display results for a single tool run
|
|
380
|
+
*
|
|
381
|
+
* @param {ToolRunResult} result - Tool run result
|
|
382
|
+
*/
|
|
383
|
+
export function displayToolResult(result) {
|
|
384
|
+
if (result.skipped) {
|
|
385
|
+
logger.warning(`⚠️ ${result.tool} — skipped`);
|
|
386
|
+
if (result.skipReason) {
|
|
387
|
+
logger.info(` 💡 ${result.skipReason}`);
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (result.errors.length === 0 && result.warnings.length === 0) {
|
|
393
|
+
logger.info(` ✅ ${result.tool} — no issues`);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (result.fixedCount > 0) {
|
|
398
|
+
logger.info(
|
|
399
|
+
` 🔧 ${result.tool} — auto-fixed ${result.fixedCount} file(s)`
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
for (const issue of result.errors) {
|
|
404
|
+
const location = issue.file
|
|
405
|
+
? `${issue.file}${issue.line ? `:${issue.line}` : ''}`
|
|
406
|
+
: '';
|
|
407
|
+
const rule = issue.ruleId ? ` (${issue.ruleId})` : '';
|
|
408
|
+
console.log(` ❌ ${location} ${issue.message}${rule}`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
for (const issue of result.warnings) {
|
|
412
|
+
const location = issue.file
|
|
413
|
+
? `${issue.file}${issue.line ? `:${issue.line}` : ''}`
|
|
414
|
+
: '';
|
|
415
|
+
const rule = issue.ruleId ? ` (${issue.ruleId})` : '';
|
|
416
|
+
console.log(` ⚠️ ${location} ${issue.message}${rule}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
package/package.json
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "claude-git-hooks",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Git hooks with Claude CLI for code analysis and automatic commit messages",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"claude-hooks": "./bin/claude-hooks"
|
|
8
|
-
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"test": "npm run test:all",
|
|
11
|
-
"test:all": "npm run lint && npm run test:smoke && npm run test:unit && npm run test:integration",
|
|
12
|
-
"test:smoke": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/smoke --maxWorkers=1",
|
|
13
|
-
"test:unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --forceExit",
|
|
14
|
-
"test:integration": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration --maxWorkers=1 --testTimeout=30000 --forceExit",
|
|
15
|
-
"test:integration:ci": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration/ci-safe.test.js --maxWorkers=1 --testTimeout=30000 --forceExit",
|
|
16
|
-
"test:changed": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --changedSince=main --forceExit",
|
|
17
|
-
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
|
|
18
|
-
"test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
|
|
19
|
-
"lint": "eslint lib/ bin/claude-hooks",
|
|
20
|
-
"lint:fix": "eslint lib/ bin/claude-hooks --fix",
|
|
21
|
-
"format": "prettier --write \"lib/**/*.js\" \"bin/**\" \"test/**/*.js\"",
|
|
22
|
-
"precommit": "npm run lint && npm run test:smoke",
|
|
23
|
-
"prepublishOnly": "npm run test:all"
|
|
24
|
-
},
|
|
25
|
-
"keywords": [
|
|
26
|
-
"git",
|
|
27
|
-
"hooks",
|
|
28
|
-
"claude",
|
|
29
|
-
"ai",
|
|
30
|
-
"code-review",
|
|
31
|
-
"commit-messages",
|
|
32
|
-
"pre-commit",
|
|
33
|
-
"automation"
|
|
34
|
-
],
|
|
35
|
-
"author": "Pablo Rovito",
|
|
36
|
-
"license": "MIT",
|
|
37
|
-
"repository": {
|
|
38
|
-
"type": "git",
|
|
39
|
-
"url": "https://github.com/mscope-S-L/git-hooks.git"
|
|
40
|
-
},
|
|
41
|
-
"engines": {
|
|
42
|
-
"node": ">=16.9.0"
|
|
43
|
-
},
|
|
44
|
-
"engineStrict": false,
|
|
45
|
-
"os": [
|
|
46
|
-
"darwin",
|
|
47
|
-
"linux",
|
|
48
|
-
"win32"
|
|
49
|
-
],
|
|
50
|
-
"preferGlobal": true,
|
|
51
|
-
"files": [
|
|
52
|
-
"bin/",
|
|
53
|
-
"lib/",
|
|
54
|
-
"templates/",
|
|
55
|
-
"README.md",
|
|
56
|
-
"CHANGELOG.md",
|
|
57
|
-
"CLAUDE.md",
|
|
58
|
-
"LICENSE"
|
|
59
|
-
],
|
|
60
|
-
"dependencies": {
|
|
61
|
-
"@octokit/rest": "^21.0.0"
|
|
62
|
-
},
|
|
63
|
-
"devDependencies": {
|
|
64
|
-
"@types/jest": "^29.5.0",
|
|
65
|
-
"eslint": "^8.57.
|
|
66
|
-
"jest": "^29.7.0",
|
|
67
|
-
"prettier": "^3.2.0"
|
|
68
|
-
}
|
|
69
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-git-hooks",
|
|
3
|
+
"version": "2.34.0",
|
|
4
|
+
"description": "Git hooks with Claude CLI for code analysis and automatic commit messages",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"claude-hooks": "./bin/claude-hooks"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "npm run test:all",
|
|
11
|
+
"test:all": "npm run lint && npm run test:smoke && npm run test:unit && npm run test:integration",
|
|
12
|
+
"test:smoke": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/smoke --maxWorkers=1",
|
|
13
|
+
"test:unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --forceExit",
|
|
14
|
+
"test:integration": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration --maxWorkers=1 --testTimeout=30000 --forceExit",
|
|
15
|
+
"test:integration:ci": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration/ci-safe.test.js --maxWorkers=1 --testTimeout=30000 --forceExit",
|
|
16
|
+
"test:changed": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --changedSince=main --forceExit",
|
|
17
|
+
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
|
|
18
|
+
"test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
|
|
19
|
+
"lint": "eslint lib/ bin/claude-hooks",
|
|
20
|
+
"lint:fix": "eslint lib/ bin/claude-hooks --fix",
|
|
21
|
+
"format": "prettier --write \"lib/**/*.js\" \"bin/**\" \"test/**/*.js\"",
|
|
22
|
+
"precommit": "npm run lint && npm run test:smoke",
|
|
23
|
+
"prepublishOnly": "npm run test:all"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"git",
|
|
27
|
+
"hooks",
|
|
28
|
+
"claude",
|
|
29
|
+
"ai",
|
|
30
|
+
"code-review",
|
|
31
|
+
"commit-messages",
|
|
32
|
+
"pre-commit",
|
|
33
|
+
"automation"
|
|
34
|
+
],
|
|
35
|
+
"author": "Pablo Rovito",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/mscope-S-L/git-hooks.git"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=16.9.0"
|
|
43
|
+
},
|
|
44
|
+
"engineStrict": false,
|
|
45
|
+
"os": [
|
|
46
|
+
"darwin",
|
|
47
|
+
"linux",
|
|
48
|
+
"win32"
|
|
49
|
+
],
|
|
50
|
+
"preferGlobal": true,
|
|
51
|
+
"files": [
|
|
52
|
+
"bin/",
|
|
53
|
+
"lib/",
|
|
54
|
+
"templates/",
|
|
55
|
+
"README.md",
|
|
56
|
+
"CHANGELOG.md",
|
|
57
|
+
"CLAUDE.md",
|
|
58
|
+
"LICENSE"
|
|
59
|
+
],
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"@octokit/rest": "^21.0.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@types/jest": "^29.5.0",
|
|
65
|
+
"eslint": "^8.57.1",
|
|
66
|
+
"jest": "^29.7.0",
|
|
67
|
+
"prettier": "^3.2.0"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -47,6 +47,14 @@
|
|
|
47
47
|
"documentation",
|
|
48
48
|
"testing"
|
|
49
49
|
]
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
"linting": {
|
|
53
|
+
"enabled": true,
|
|
54
|
+
"autoFix": true,
|
|
55
|
+
"failOnError": true,
|
|
56
|
+
"failOnWarning": false,
|
|
57
|
+
"timeout": 30000
|
|
50
58
|
}
|
|
51
59
|
},
|
|
52
60
|
|
|
@@ -105,6 +113,36 @@
|
|
|
105
113
|
"description": "Categories for general (review-level) comments",
|
|
106
114
|
"default": "[\"ticket-alignment\", \"scope\", \"style\", \"good-practice\", \"extensibility\", \"observability\", \"documentation\", \"testing\"]",
|
|
107
115
|
"use_case": "Customize which observation types appear in the review body"
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
"linting.enabled": {
|
|
119
|
+
"description": "Enable/disable linter execution before Claude analysis in pre-commit hook and lint command",
|
|
120
|
+
"default": "true",
|
|
121
|
+
"use_case": "Set to false to skip linting entirely"
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
"linting.autoFix": {
|
|
125
|
+
"description": "Automatically fix linting issues and re-stage files",
|
|
126
|
+
"default": "true",
|
|
127
|
+
"use_case": "Set to false to only report issues without fixing"
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
"linting.failOnError": {
|
|
131
|
+
"description": "Block commit when linter reports errors",
|
|
132
|
+
"default": "true",
|
|
133
|
+
"use_case": "Set to false to allow commits with linting errors (not recommended)"
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
"linting.failOnWarning": {
|
|
137
|
+
"description": "Block commit when linter reports warnings",
|
|
138
|
+
"default": "false",
|
|
139
|
+
"use_case": "Set to true for strict projects that treat warnings as errors"
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
"linting.timeout": {
|
|
143
|
+
"description": "Timeout in milliseconds for each linter execution",
|
|
144
|
+
"default": "30000",
|
|
145
|
+
"use_case": "Increase for large projects where linters take longer"
|
|
108
146
|
}
|
|
109
147
|
},
|
|
110
148
|
|