claude-git-hooks 1.5.4 → 1.5.5
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 +36 -1
- package/README.md +40 -0
- package/bin/claude-hooks +165 -108
- package/package.json +1 -1
- package/templates/pre-commit +35 -1
- package/templates/prepare-commit-msg +34 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,42 @@ 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
|
-
## [1.5.
|
|
8
|
+
## [1.5.5] - 2025-10-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- 🚀 **Parallel Analysis with Subagents**
|
|
13
|
+
- New `CLAUDE_USE_SUBAGENTS` environment variable to enable parallel file analysis
|
|
14
|
+
- Each file analyzed by dedicated Claude subagent for faster processing
|
|
15
|
+
- Works across all analysis functions: pre-commit, message generation, and analyze-diff
|
|
16
|
+
- Significantly faster for commits with 3+ files
|
|
17
|
+
|
|
18
|
+
- ⚙️ **Subagent Configuration Options**
|
|
19
|
+
- `CLAUDE_SUBAGENT_MODEL` - Choose model: haiku (fast), sonnet (balanced), opus (thorough)
|
|
20
|
+
- `CLAUDE_SUBAGENT_BATCH_SIZE` - Control parallel processing (default: 3 files at once)
|
|
21
|
+
- Automatic result consolidation across all subagents
|
|
22
|
+
|
|
23
|
+
- ⏱️ **Execution Time Measurement**
|
|
24
|
+
- All operations now display execution time in seconds and milliseconds
|
|
25
|
+
- Pre-commit analysis: Shows time on success and failure
|
|
26
|
+
- Message generation: Shows generation time
|
|
27
|
+
- Analyze-diff: Shows analysis time
|
|
28
|
+
- Helps identify performance improvements and bottlenecks
|
|
29
|
+
|
|
30
|
+
- 🔧 **Git Worktree Support**
|
|
31
|
+
- `claude-hooks` now recognizes worktrees created in PowerShell from WSL
|
|
32
|
+
- Automatically converts Windows paths (C:\) to WSL paths (/mnt/c/)
|
|
33
|
+
- No more "not a git repository" errors when working in cross-platform worktrees
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- 📝 Subagent instructions include explicit consolidation logic for consistent results
|
|
38
|
+
- 🎨 Help command updated with batching examples and clarifications
|
|
39
|
+
- 📋 Post-install text now mentions parallel analysis feature with examples
|
|
40
|
+
- ✅ **Batch size validation** - `CLAUDE_SUBAGENT_BATCH_SIZE` now validated (minimum: 1)
|
|
41
|
+
- 📚 Batching behavior explicitly documented with concrete examples
|
|
42
|
+
|
|
43
|
+
## [1.5.4] - 2025-09-15
|
|
9
44
|
|
|
10
45
|
### Fixed
|
|
11
46
|
|
package/README.md
CHANGED
|
@@ -101,6 +101,11 @@ public void methodToIgnore() {
|
|
|
101
101
|
# Variables de entorno
|
|
102
102
|
export CLAUDE_ANALYSIS_MODE=sonarqube # Modo de análisis
|
|
103
103
|
export CLAUDE_DEBUG=true # Debug detallado
|
|
104
|
+
|
|
105
|
+
# Subagent configuration (parallel analysis for multiple files)
|
|
106
|
+
export CLAUDE_USE_SUBAGENTS=true # Enable subagent parallel analysis
|
|
107
|
+
export CLAUDE_SUBAGENT_MODEL=haiku # Model: haiku (fast), sonnet (balanced), opus (thorough)
|
|
108
|
+
export CLAUDE_SUBAGENT_BATCH_SIZE=3 # Parallel subagents per batch (default: 3)
|
|
104
109
|
```
|
|
105
110
|
|
|
106
111
|
### 🎯 Casos de Uso Específicos
|
|
@@ -134,6 +139,41 @@ git commit -m "fix: resolver issues"
|
|
|
134
139
|
5. **Archivos grandes**: Se omiten automáticamente archivos > 100KB
|
|
135
140
|
6. **Límite de archivos**: Máximo 10 archivos por commit
|
|
136
141
|
|
|
142
|
+
### 🚀 Parallel Analysis with Subagents (NEW in v1.5.5)
|
|
143
|
+
|
|
144
|
+
**When analyzing 3+ files**, enable subagents for faster parallel processing:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# Enable subagent parallel analysis
|
|
148
|
+
export CLAUDE_USE_SUBAGENTS=true
|
|
149
|
+
|
|
150
|
+
# Optional: Choose model (default: haiku for speed)
|
|
151
|
+
export CLAUDE_SUBAGENT_MODEL=haiku # Fast & cheap
|
|
152
|
+
export CLAUDE_SUBAGENT_MODEL=sonnet # Balanced quality/speed
|
|
153
|
+
export CLAUDE_SUBAGENT_MODEL=opus # Maximum quality
|
|
154
|
+
|
|
155
|
+
# Optional: Adjust batch size (default: 3 files at a time)
|
|
156
|
+
export CLAUDE_SUBAGENT_BATCH_SIZE=3
|
|
157
|
+
|
|
158
|
+
# Now commits will use parallel analysis automatically
|
|
159
|
+
git commit -m "feat: implement multiple features"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**How batching works:**
|
|
163
|
+
- `BATCH_SIZE=1` with 4 files → 4 sequential batches (1 subagent each)
|
|
164
|
+
- `BATCH_SIZE=3` with 4 files → 2 batches (3 parallel, then 1)
|
|
165
|
+
- `BATCH_SIZE=4` with 4 files → 1 batch (all 4 in parallel)
|
|
166
|
+
- Batch size is validated automatically (minimum: 1)
|
|
167
|
+
|
|
168
|
+
**How it works:**
|
|
169
|
+
- Each file is analyzed by a dedicated Claude subagent
|
|
170
|
+
- Files are processed in batches for optimal performance
|
|
171
|
+
- Results are consolidated automatically into single response
|
|
172
|
+
- Shows execution time for all operations
|
|
173
|
+
- Uses one-line prompt injection - no complex architecture changes
|
|
174
|
+
|
|
175
|
+
**Best for:** Large commits (3+ files), refactoring tasks, feature branches
|
|
176
|
+
|
|
137
177
|
## 🔧 Configuración Previa Importante
|
|
138
178
|
|
|
139
179
|
### Credenciales Git en WSL
|
package/bin/claude-hooks
CHANGED
|
@@ -42,24 +42,24 @@ async function checkVersionAndPromptUpdate() {
|
|
|
42
42
|
try {
|
|
43
43
|
const currentVersion = require('../package.json').version;
|
|
44
44
|
const latestVersion = await getLatestVersion('claude-git-hooks');
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
if (currentVersion === latestVersion) {
|
|
47
47
|
return true; // Already updated
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
|
|
50
50
|
console.log('');
|
|
51
51
|
warning(`New version available: ${latestVersion} (current: ${currentVersion})`);
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
// Interactive prompt compatible with all consoles
|
|
54
54
|
const rl = readline.createInterface({
|
|
55
55
|
input: process.stdin,
|
|
56
56
|
output: process.stdout
|
|
57
57
|
});
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
return new Promise((resolve) => {
|
|
60
60
|
rl.question('Do you want to update now? (y/n): ', (answer) => {
|
|
61
61
|
rl.close();
|
|
62
|
-
|
|
62
|
+
|
|
63
63
|
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
|
64
64
|
info('Updating claude-git-hooks...');
|
|
65
65
|
try {
|
|
@@ -124,7 +124,7 @@ function readPassword(prompt) {
|
|
|
124
124
|
input: process.stdin,
|
|
125
125
|
output: process.stdout
|
|
126
126
|
});
|
|
127
|
-
|
|
127
|
+
|
|
128
128
|
// Disable echo
|
|
129
129
|
rl.stdoutMuted = true;
|
|
130
130
|
rl._writeToOutput = function _writeToOutput(stringToWrite) {
|
|
@@ -133,7 +133,7 @@ function readPassword(prompt) {
|
|
|
133
133
|
else
|
|
134
134
|
rl.output.write(stringToWrite);
|
|
135
135
|
};
|
|
136
|
-
|
|
136
|
+
|
|
137
137
|
rl.question(prompt, (password) => {
|
|
138
138
|
rl.close();
|
|
139
139
|
console.log(); // New line
|
|
@@ -145,7 +145,7 @@ function readPassword(prompt) {
|
|
|
145
145
|
// Check if sudo password is correct
|
|
146
146
|
function testSudoPassword(password) {
|
|
147
147
|
try {
|
|
148
|
-
execSync('echo "' + password + '" | sudo -S true', {
|
|
148
|
+
execSync('echo "' + password + '" | sudo -S true', {
|
|
149
149
|
stdio: 'ignore',
|
|
150
150
|
timeout: 5000
|
|
151
151
|
});
|
|
@@ -231,26 +231,26 @@ class Entertainment {
|
|
|
231
231
|
// Get first joke from API without blocking
|
|
232
232
|
this.getJoke().then(joke => {
|
|
233
233
|
if (!isFinished) currentJoke = joke;
|
|
234
|
-
}).catch(() => {}); // If it fails, keep the local one
|
|
234
|
+
}).catch(() => { }); // If it fails, keep the local one
|
|
235
235
|
|
|
236
236
|
// Hide cursor
|
|
237
237
|
process.stdout.write('\x1B[?25l');
|
|
238
|
-
|
|
238
|
+
|
|
239
239
|
// Reserve space for the 3 lines
|
|
240
240
|
process.stdout.write('\n\n\n');
|
|
241
|
-
|
|
241
|
+
|
|
242
242
|
const interval = setInterval(() => {
|
|
243
243
|
if (isFinished) {
|
|
244
244
|
clearInterval(interval);
|
|
245
245
|
return;
|
|
246
246
|
}
|
|
247
|
-
|
|
247
|
+
|
|
248
248
|
spinnerIndex++;
|
|
249
|
-
|
|
249
|
+
|
|
250
250
|
// Update countdown every second (10 iterations of 100ms)
|
|
251
251
|
if (spinnerIndex % 10 === 0) {
|
|
252
252
|
jokeCountdown--;
|
|
253
|
-
|
|
253
|
+
|
|
254
254
|
// Refresh joke every 10 seconds
|
|
255
255
|
if (jokeCountdown <= 0) {
|
|
256
256
|
this.getJoke().then(joke => {
|
|
@@ -263,19 +263,19 @@ class Entertainment {
|
|
|
263
263
|
jokeCountdown = 10;
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
|
-
|
|
266
|
+
|
|
267
267
|
// Always go back exactly 3 lines up
|
|
268
268
|
process.stdout.write('\x1B[3A');
|
|
269
|
-
|
|
269
|
+
|
|
270
270
|
// Render the 3 lines from the beginning
|
|
271
271
|
const spinner = spinners[spinnerIndex % spinners.length];
|
|
272
|
-
|
|
272
|
+
|
|
273
273
|
// Line 1: Spinner
|
|
274
274
|
process.stdout.write('\r\x1B[2K' + `${colors.yellow}${spinner} ${message}${colors.reset}\n`);
|
|
275
|
-
|
|
275
|
+
|
|
276
276
|
// Line 2: Joke
|
|
277
277
|
process.stdout.write('\r\x1B[2K' + `${colors.green}🎭 ${currentJoke}${colors.reset}\n`);
|
|
278
|
-
|
|
278
|
+
|
|
279
279
|
// Line 3: Countdown
|
|
280
280
|
process.stdout.write('\r\x1B[2K' + `${colors.yellow}⏱️ Next joke in: ${jokeCountdown}s${colors.reset}\n`);
|
|
281
281
|
}, 100);
|
|
@@ -284,7 +284,7 @@ class Entertainment {
|
|
|
284
284
|
const result = await promise;
|
|
285
285
|
isFinished = true;
|
|
286
286
|
clearInterval(interval);
|
|
287
|
-
|
|
287
|
+
|
|
288
288
|
// Clean exactly 3 lines completely
|
|
289
289
|
process.stdout.write('\x1B[3A'); // Go up 3 lines
|
|
290
290
|
process.stdout.write('\r\x1B[2K'); // Clean line 1
|
|
@@ -292,15 +292,15 @@ class Entertainment {
|
|
|
292
292
|
process.stdout.write('\n\r\x1B[2K'); // Go down and clean line 3
|
|
293
293
|
process.stdout.write('\x1B[2A'); // Go up 2 lines to end up on the first
|
|
294
294
|
process.stdout.write('\r'); // Go to beginning of line
|
|
295
|
-
|
|
295
|
+
|
|
296
296
|
// Show cursor
|
|
297
297
|
process.stdout.write('\x1B[?25h');
|
|
298
|
-
|
|
298
|
+
|
|
299
299
|
return result;
|
|
300
300
|
} catch (error) {
|
|
301
301
|
isFinished = true;
|
|
302
302
|
clearInterval(interval);
|
|
303
|
-
|
|
303
|
+
|
|
304
304
|
// Clean exactly 3 lines completely
|
|
305
305
|
process.stdout.write('\x1B[3A'); // Go up 3 lines
|
|
306
306
|
process.stdout.write('\r\x1B[2K'); // Clean line 1
|
|
@@ -308,21 +308,42 @@ class Entertainment {
|
|
|
308
308
|
process.stdout.write('\n\r\x1B[2K'); // Go down and clean line 3
|
|
309
309
|
process.stdout.write('\x1B[2A'); // Go up 2 lines to end up on the first
|
|
310
310
|
process.stdout.write('\r'); // Go to beginning of line
|
|
311
|
-
|
|
311
|
+
|
|
312
312
|
// Show cursor
|
|
313
313
|
process.stdout.write('\x1B[?25h');
|
|
314
|
-
|
|
314
|
+
|
|
315
315
|
throw error;
|
|
316
316
|
}
|
|
317
317
|
}
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
-
// Check if we are in a git repository
|
|
320
|
+
// Check if we are in a git repository (including worktrees created in PowerShell)
|
|
321
321
|
function checkGitRepo() {
|
|
322
322
|
try {
|
|
323
323
|
execSync('git rev-parse --git-dir', { stdio: 'ignore' });
|
|
324
324
|
return true;
|
|
325
325
|
} catch (e) {
|
|
326
|
+
// Try to detect worktree created in PowerShell
|
|
327
|
+
try {
|
|
328
|
+
if (fs.existsSync('.git')) {
|
|
329
|
+
const gitContent = fs.readFileSync('.git', 'utf8').trim();
|
|
330
|
+
// Check if it's a worktree pointer (gitdir: ...)
|
|
331
|
+
if (gitContent.startsWith('gitdir:')) {
|
|
332
|
+
let gitdir = gitContent.substring(8).trim();
|
|
333
|
+
// Convert Windows path to WSL if needed (C:\ -> /mnt/c/)
|
|
334
|
+
if (/^[A-Za-z]:/.test(gitdir)) {
|
|
335
|
+
gitdir = gitdir.replace(/^([A-Za-z]):/, (_, drive) => `/mnt/${drive.toLowerCase()}`);
|
|
336
|
+
gitdir = gitdir.replace(/\\/g, '/');
|
|
337
|
+
}
|
|
338
|
+
// Verify the gitdir exists
|
|
339
|
+
if (fs.existsSync(gitdir)) {
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
} catch (worktreeError) {
|
|
345
|
+
// Ignore worktree detection errors
|
|
346
|
+
}
|
|
326
347
|
return false;
|
|
327
348
|
}
|
|
328
349
|
}
|
|
@@ -340,7 +361,7 @@ async function install(args) {
|
|
|
340
361
|
|
|
341
362
|
const isForce = args.includes('--force');
|
|
342
363
|
const skipAuth = args.includes('--skip-auth');
|
|
343
|
-
|
|
364
|
+
|
|
344
365
|
if (isForce) {
|
|
345
366
|
info('Installing Claude Git Hooks (force mode)...');
|
|
346
367
|
} else {
|
|
@@ -354,7 +375,7 @@ async function install(args) {
|
|
|
354
375
|
if (needsInstall) {
|
|
355
376
|
info('Sudo access is needed for automatic dependency installation, please enter password');
|
|
356
377
|
sudoPassword = await readPassword('Enter your Ubuntu password for sudo: ');
|
|
357
|
-
|
|
378
|
+
|
|
358
379
|
if (sudoPassword && !testSudoPassword(sudoPassword)) {
|
|
359
380
|
warning('Incorrect password. Continuing without automatic installation.');
|
|
360
381
|
sudoPassword = null;
|
|
@@ -377,18 +398,18 @@ async function install(args) {
|
|
|
377
398
|
|
|
378
399
|
// Hooks to install
|
|
379
400
|
const hooks = ['pre-commit', 'prepare-commit-msg'];
|
|
380
|
-
|
|
401
|
+
|
|
381
402
|
hooks.forEach(hook => {
|
|
382
403
|
const sourcePath = path.join(templatesPath, hook);
|
|
383
404
|
const destPath = path.join(hooksPath, hook);
|
|
384
|
-
|
|
405
|
+
|
|
385
406
|
// Make backup if it exists
|
|
386
407
|
if (fs.existsSync(destPath)) {
|
|
387
408
|
const backupPath = `${destPath}.backup.${Date.now()}`;
|
|
388
409
|
fs.copyFileSync(destPath, backupPath);
|
|
389
410
|
info(`Backup created: ${backupPath}`);
|
|
390
411
|
}
|
|
391
|
-
|
|
412
|
+
|
|
392
413
|
// Copy hook
|
|
393
414
|
fs.copyFileSync(sourcePath, destPath);
|
|
394
415
|
fs.chmodSync(destPath, '755');
|
|
@@ -398,7 +419,7 @@ async function install(args) {
|
|
|
398
419
|
// Copy version verification script
|
|
399
420
|
const checkVersionSource = path.join(templatesPath, 'check-version.sh');
|
|
400
421
|
const checkVersionDest = path.join(hooksPath, 'check-version.sh');
|
|
401
|
-
|
|
422
|
+
|
|
402
423
|
if (fs.existsSync(checkVersionSource)) {
|
|
403
424
|
fs.copyFileSync(checkVersionSource, checkVersionDest);
|
|
404
425
|
fs.chmodSync(checkVersionDest, '755');
|
|
@@ -418,11 +439,11 @@ async function install(args) {
|
|
|
418
439
|
'CLAUDE_ANALYSIS_PROMPT_SONAR.md',
|
|
419
440
|
'CLAUDE_RESOLUTION_PROMPT.md'
|
|
420
441
|
];
|
|
421
|
-
|
|
442
|
+
|
|
422
443
|
claudeFiles.forEach(file => {
|
|
423
444
|
const destPath = path.join(claudeDir, file);
|
|
424
445
|
const sourcePath = path.join(templatesPath, file);
|
|
425
|
-
|
|
446
|
+
|
|
426
447
|
// In force mode or if it doesn't exist, copy the file
|
|
427
448
|
if (isForce || !fs.existsSync(destPath)) {
|
|
428
449
|
if (fs.existsSync(sourcePath)) {
|
|
@@ -450,13 +471,19 @@ async function install(args) {
|
|
|
450
471
|
console.log(' // SKIP_ANALYSIS_BLOCK # Exclude block until finding another equal one');
|
|
451
472
|
console.log(' ...excluded code...');
|
|
452
473
|
console.log(' // SKIP_ANALYSIS_BLOCK');
|
|
474
|
+
console.log('\nNEW: Parallel analysis for faster multi-file commits:');
|
|
475
|
+
console.log(' export CLAUDE_USE_SUBAGENTS=true # Enable subagents');
|
|
476
|
+
console.log(' export CLAUDE_SUBAGENT_MODEL=haiku # haiku/sonnet/opus');
|
|
477
|
+
console.log(' export CLAUDE_SUBAGENT_BATCH_SIZE=3 # Parallel per batch (default: 3)');
|
|
478
|
+
console.log(' # Example: 4 files, BATCH_SIZE=1 → 4 sequential batches');
|
|
479
|
+
console.log(' # Example: 4 files, BATCH_SIZE=3 → 2 batches (3 parallel + 1)');
|
|
453
480
|
console.log('\nFor more options: claude-hooks --help');
|
|
454
481
|
}
|
|
455
482
|
|
|
456
483
|
// Check complete dependencies (like setup-wsl.sh)
|
|
457
484
|
async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false) {
|
|
458
485
|
info('Checking system dependencies...');
|
|
459
|
-
|
|
486
|
+
|
|
460
487
|
// Check Node.js
|
|
461
488
|
try {
|
|
462
489
|
const nodeVersion = execSync('node --version', { encoding: 'utf8' }).trim();
|
|
@@ -464,7 +491,7 @@ async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false
|
|
|
464
491
|
} catch (e) {
|
|
465
492
|
error('Node.js is not installed. Install Node.js and try again.');
|
|
466
493
|
}
|
|
467
|
-
|
|
494
|
+
|
|
468
495
|
// Check npm
|
|
469
496
|
try {
|
|
470
497
|
const npmVersion = execSync('npm --version', { encoding: 'utf8' }).trim();
|
|
@@ -472,7 +499,7 @@ async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false
|
|
|
472
499
|
} catch (e) {
|
|
473
500
|
error('npm is not installed.');
|
|
474
501
|
}
|
|
475
|
-
|
|
502
|
+
|
|
476
503
|
// Check and install jq
|
|
477
504
|
try {
|
|
478
505
|
const jqVersion = execSync('jq --version', { encoding: 'utf8' }).trim();
|
|
@@ -490,7 +517,7 @@ async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false
|
|
|
490
517
|
}
|
|
491
518
|
}
|
|
492
519
|
}
|
|
493
|
-
|
|
520
|
+
|
|
494
521
|
// Check and install curl
|
|
495
522
|
try {
|
|
496
523
|
const curlVersion = execSync('curl --version', { encoding: 'utf8' }).split('\n')[0];
|
|
@@ -508,7 +535,7 @@ async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false
|
|
|
508
535
|
}
|
|
509
536
|
}
|
|
510
537
|
}
|
|
511
|
-
|
|
538
|
+
|
|
512
539
|
// Check Git
|
|
513
540
|
try {
|
|
514
541
|
const gitVersion = execSync('git --version', { encoding: 'utf8' }).trim();
|
|
@@ -516,11 +543,11 @@ async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false
|
|
|
516
543
|
} catch (e) {
|
|
517
544
|
error('Git is not installed. Install Git and try again.');
|
|
518
545
|
}
|
|
519
|
-
|
|
546
|
+
|
|
520
547
|
// Check standard Unix tools
|
|
521
548
|
const unixTools = ['sed', 'awk', 'grep', 'head', 'tail', 'stat', 'tput'];
|
|
522
549
|
const missingTools = [];
|
|
523
|
-
|
|
550
|
+
|
|
524
551
|
unixTools.forEach(tool => {
|
|
525
552
|
try {
|
|
526
553
|
execSync(`which ${tool}`, { stdio: 'ignore' });
|
|
@@ -528,23 +555,23 @@ async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false
|
|
|
528
555
|
missingTools.push(tool);
|
|
529
556
|
}
|
|
530
557
|
});
|
|
531
|
-
|
|
558
|
+
|
|
532
559
|
if (missingTools.length === 0) {
|
|
533
560
|
success('Standard Unix tools verified');
|
|
534
561
|
} else {
|
|
535
562
|
error(`Missing standard Unix tools: ${missingTools.join(', ')}. Retry installation in an Ubuntu console`);
|
|
536
563
|
}
|
|
537
|
-
|
|
564
|
+
|
|
538
565
|
// Check and install Claude CLI
|
|
539
566
|
await checkAndInstallClaude();
|
|
540
|
-
|
|
567
|
+
|
|
541
568
|
// Check Claude authentication (if not skipped)
|
|
542
569
|
if (!skipAuth) {
|
|
543
570
|
await checkClaudeAuth();
|
|
544
571
|
} else {
|
|
545
572
|
warning('Skipping Claude authentication verification (--skip-auth)');
|
|
546
573
|
}
|
|
547
|
-
|
|
574
|
+
|
|
548
575
|
// Clear password from memory
|
|
549
576
|
sudoPassword = null;
|
|
550
577
|
}
|
|
@@ -552,7 +579,7 @@ async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false
|
|
|
552
579
|
// Check if we need to install dependencies
|
|
553
580
|
async function checkIfInstallationNeeded() {
|
|
554
581
|
const dependencies = ['jq', 'curl'];
|
|
555
|
-
|
|
582
|
+
|
|
556
583
|
for (const dep of dependencies) {
|
|
557
584
|
try {
|
|
558
585
|
execSync(`which ${dep}`, { stdio: 'ignore' });
|
|
@@ -560,14 +587,14 @@ async function checkIfInstallationNeeded() {
|
|
|
560
587
|
return true; // Needs installation
|
|
561
588
|
}
|
|
562
589
|
}
|
|
563
|
-
|
|
590
|
+
|
|
564
591
|
// Verificar Claude CLI
|
|
565
592
|
try {
|
|
566
593
|
execSync('claude --version', { stdio: 'ignore' });
|
|
567
594
|
} catch (e) {
|
|
568
595
|
return true; // Needs Claude installation
|
|
569
596
|
}
|
|
570
|
-
|
|
597
|
+
|
|
571
598
|
return false;
|
|
572
599
|
}
|
|
573
600
|
|
|
@@ -590,7 +617,7 @@ async function checkAndInstallClaude() {
|
|
|
590
617
|
// Check Claude authentication with entertainment
|
|
591
618
|
async function checkClaudeAuth() {
|
|
592
619
|
info('Checking Claude authentication...');
|
|
593
|
-
|
|
620
|
+
|
|
594
621
|
// Use spawn to not block, but with stdio: 'ignore' like the original
|
|
595
622
|
const authPromise = new Promise((resolve, reject) => {
|
|
596
623
|
const child = spawn('claude', ['auth', 'status'], {
|
|
@@ -598,13 +625,13 @@ async function checkClaudeAuth() {
|
|
|
598
625
|
detached: false,
|
|
599
626
|
windowsHide: true
|
|
600
627
|
});
|
|
601
|
-
|
|
628
|
+
|
|
602
629
|
// Manual timeout since spawn doesn't have native timeout
|
|
603
630
|
const timeout = setTimeout(() => {
|
|
604
631
|
child.kill();
|
|
605
632
|
reject(new Error('timeout'));
|
|
606
633
|
}, 120000); // 2 minutos
|
|
607
|
-
|
|
634
|
+
|
|
608
635
|
child.on('exit', (code) => {
|
|
609
636
|
clearTimeout(timeout);
|
|
610
637
|
if (code === 0) {
|
|
@@ -613,7 +640,7 @@ async function checkClaudeAuth() {
|
|
|
613
640
|
reject(new Error('not_authenticated'));
|
|
614
641
|
}
|
|
615
642
|
});
|
|
616
|
-
|
|
643
|
+
|
|
617
644
|
child.on('error', (err) => {
|
|
618
645
|
clearTimeout(timeout);
|
|
619
646
|
reject(err);
|
|
@@ -633,7 +660,7 @@ async function checkClaudeAuth() {
|
|
|
633
660
|
// Update .gitignore with Claude entries
|
|
634
661
|
function updateGitignore() {
|
|
635
662
|
info('Updating .gitignore...');
|
|
636
|
-
|
|
663
|
+
|
|
637
664
|
const gitignorePath = '.gitignore';
|
|
638
665
|
const claudeEntries = [
|
|
639
666
|
'# Claude Git Hooks',
|
|
@@ -642,16 +669,16 @@ function updateGitignore() {
|
|
|
642
669
|
'claude_resolution_prompt.md',
|
|
643
670
|
'.claude-pr-analysis.json',
|
|
644
671
|
];
|
|
645
|
-
|
|
672
|
+
|
|
646
673
|
let gitignoreContent = '';
|
|
647
674
|
let fileExists = false;
|
|
648
|
-
|
|
675
|
+
|
|
649
676
|
// Read existing .gitignore if it exists
|
|
650
677
|
if (fs.existsSync(gitignorePath)) {
|
|
651
678
|
gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
|
652
679
|
fileExists = true;
|
|
653
680
|
}
|
|
654
|
-
|
|
681
|
+
|
|
655
682
|
// Check which entries are missing
|
|
656
683
|
const missingEntries = [];
|
|
657
684
|
claudeEntries.forEach(entry => {
|
|
@@ -668,31 +695,31 @@ function updateGitignore() {
|
|
|
668
695
|
}
|
|
669
696
|
}
|
|
670
697
|
});
|
|
671
|
-
|
|
698
|
+
|
|
672
699
|
// If there are missing entries, add them
|
|
673
700
|
if (missingEntries.length > 0) {
|
|
674
701
|
// Ensure there's a newline at the end if the file exists and is not empty
|
|
675
702
|
if (fileExists && gitignoreContent.length > 0 && !gitignoreContent.endsWith('\n')) {
|
|
676
703
|
gitignoreContent += '\n';
|
|
677
704
|
}
|
|
678
|
-
|
|
705
|
+
|
|
679
706
|
// If the file is not empty, add a blank line before
|
|
680
707
|
if (gitignoreContent.length > 0) {
|
|
681
708
|
gitignoreContent += '\n';
|
|
682
709
|
}
|
|
683
|
-
|
|
710
|
+
|
|
684
711
|
// Add the missing entries
|
|
685
712
|
gitignoreContent += missingEntries.join('\n') + '\n';
|
|
686
|
-
|
|
713
|
+
|
|
687
714
|
// Write the updated file
|
|
688
715
|
fs.writeFileSync(gitignorePath, gitignoreContent);
|
|
689
|
-
|
|
716
|
+
|
|
690
717
|
if (fileExists) {
|
|
691
718
|
success('.gitignore updated with Claude entries');
|
|
692
719
|
} else {
|
|
693
720
|
success('.gitignore created with Claude entries');
|
|
694
721
|
}
|
|
695
|
-
|
|
722
|
+
|
|
696
723
|
// Show what was added
|
|
697
724
|
missingEntries.forEach(entry => {
|
|
698
725
|
if (!entry.startsWith('#')) {
|
|
@@ -707,12 +734,12 @@ function updateGitignore() {
|
|
|
707
734
|
// Configure Git (line endings, etc.)
|
|
708
735
|
function configureGit() {
|
|
709
736
|
info('Configuring Git...');
|
|
710
|
-
|
|
737
|
+
|
|
711
738
|
try {
|
|
712
739
|
// Configure line endings for WSL
|
|
713
740
|
execSync('git config core.autocrlf input', { stdio: 'ignore' });
|
|
714
741
|
success('Line endings configured for WSL (core.autocrlf = input)');
|
|
715
|
-
|
|
742
|
+
|
|
716
743
|
// Try to configure on Windows through PowerShell
|
|
717
744
|
try {
|
|
718
745
|
execSync('powershell.exe -Command "git config core.autocrlf true"', { stdio: 'ignore' });
|
|
@@ -720,7 +747,7 @@ function configureGit() {
|
|
|
720
747
|
} catch (psError) {
|
|
721
748
|
info('Could not configure automatically on Windows');
|
|
722
749
|
}
|
|
723
|
-
|
|
750
|
+
|
|
724
751
|
} catch (e) {
|
|
725
752
|
warning('Error configuring Git');
|
|
726
753
|
}
|
|
@@ -736,7 +763,7 @@ function uninstall() {
|
|
|
736
763
|
|
|
737
764
|
const hooksPath = '.git/hooks';
|
|
738
765
|
const hooks = ['pre-commit', 'prepare-commit-msg'];
|
|
739
|
-
|
|
766
|
+
|
|
740
767
|
hooks.forEach(hook => {
|
|
741
768
|
const hookPath = path.join(hooksPath, hook);
|
|
742
769
|
if (fs.existsSync(hookPath)) {
|
|
@@ -755,11 +782,11 @@ function enable(hookName) {
|
|
|
755
782
|
}
|
|
756
783
|
|
|
757
784
|
const hooks = hookName ? [hookName] : ['pre-commit', 'prepare-commit-msg'];
|
|
758
|
-
|
|
785
|
+
|
|
759
786
|
hooks.forEach(hook => {
|
|
760
787
|
const disabledPath = `.git/hooks/${hook}.disabled`;
|
|
761
788
|
const enabledPath = `.git/hooks/${hook}`;
|
|
762
|
-
|
|
789
|
+
|
|
763
790
|
if (fs.existsSync(disabledPath)) {
|
|
764
791
|
fs.renameSync(disabledPath, enabledPath);
|
|
765
792
|
success(`${hook} enabled`);
|
|
@@ -778,11 +805,11 @@ function disable(hookName) {
|
|
|
778
805
|
}
|
|
779
806
|
|
|
780
807
|
const hooks = hookName ? [hookName] : ['pre-commit', 'prepare-commit-msg'];
|
|
781
|
-
|
|
808
|
+
|
|
782
809
|
hooks.forEach(hook => {
|
|
783
810
|
const enabledPath = `.git/hooks/${hook}`;
|
|
784
811
|
const disabledPath = `.git/hooks/${hook}.disabled`;
|
|
785
|
-
|
|
812
|
+
|
|
786
813
|
if (fs.existsSync(enabledPath)) {
|
|
787
814
|
fs.renameSync(enabledPath, disabledPath);
|
|
788
815
|
success(`${hook} disabled`);
|
|
@@ -802,7 +829,7 @@ function analyzeDiff(args) {
|
|
|
802
829
|
}
|
|
803
830
|
|
|
804
831
|
const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
|
|
805
|
-
|
|
832
|
+
|
|
806
833
|
if (!currentBranch) {
|
|
807
834
|
error('You are not in a valid branch.');
|
|
808
835
|
return;
|
|
@@ -819,7 +846,7 @@ function analyzeDiff(args) {
|
|
|
819
846
|
baseBranch = `origin/${targetBranch}`;
|
|
820
847
|
compareWith = `${baseBranch}...HEAD`;
|
|
821
848
|
contextDescription = `${currentBranch} vs ${baseBranch}`;
|
|
822
|
-
|
|
849
|
+
|
|
823
850
|
// Check that the origin branch exists
|
|
824
851
|
try {
|
|
825
852
|
execSync(`git rev-parse --verify ${baseBranch}`, { stdio: 'ignore' });
|
|
@@ -832,7 +859,7 @@ function analyzeDiff(args) {
|
|
|
832
859
|
baseBranch = `origin/${currentBranch}`;
|
|
833
860
|
compareWith = `${baseBranch}...HEAD`;
|
|
834
861
|
contextDescription = `${currentBranch} vs ${baseBranch}`;
|
|
835
|
-
|
|
862
|
+
|
|
836
863
|
// Check that the origin branch exists
|
|
837
864
|
try {
|
|
838
865
|
execSync(`git rev-parse --verify ${baseBranch}`, { stdio: 'ignore' });
|
|
@@ -841,7 +868,7 @@ function analyzeDiff(args) {
|
|
|
841
868
|
baseBranch = 'origin/develop';
|
|
842
869
|
compareWith = `${baseBranch}...HEAD`;
|
|
843
870
|
contextDescription = `${currentBranch} vs ${baseBranch} (fallback)`;
|
|
844
|
-
|
|
871
|
+
|
|
845
872
|
try {
|
|
846
873
|
execSync(`git rev-parse --verify ${baseBranch}`, { stdio: 'ignore' });
|
|
847
874
|
warning(`Branch origin/${currentBranch} does not exist. Using ${baseBranch} as fallback.`);
|
|
@@ -850,7 +877,7 @@ function analyzeDiff(args) {
|
|
|
850
877
|
baseBranch = 'origin/main';
|
|
851
878
|
compareWith = `${baseBranch}...HEAD`;
|
|
852
879
|
contextDescription = `${currentBranch} vs ${baseBranch} (fallback)`;
|
|
853
|
-
|
|
880
|
+
|
|
854
881
|
try {
|
|
855
882
|
execSync(`git rev-parse --verify ${baseBranch}`, { stdio: 'ignore' });
|
|
856
883
|
warning(`No origin/develop branch. Using ${baseBranch} as fallback.`);
|
|
@@ -868,12 +895,12 @@ function analyzeDiff(args) {
|
|
|
868
895
|
let diffFiles;
|
|
869
896
|
try {
|
|
870
897
|
diffFiles = execSync(`git diff ${compareWith} --name-only`, { encoding: 'utf8' }).trim();
|
|
871
|
-
|
|
898
|
+
|
|
872
899
|
if (!diffFiles) {
|
|
873
900
|
// Check if there are staged or unstaged changes
|
|
874
901
|
const stagedFiles = execSync('git diff --cached --name-only', { encoding: 'utf8' }).trim();
|
|
875
902
|
const unstagedFiles = execSync('git diff --name-only', { encoding: 'utf8' }).trim();
|
|
876
|
-
|
|
903
|
+
|
|
877
904
|
if (stagedFiles || unstagedFiles) {
|
|
878
905
|
warning('No differences with remote, but you have uncommitted local changes.');
|
|
879
906
|
console.log('Staged changes:', stagedFiles || 'none');
|
|
@@ -902,14 +929,26 @@ function analyzeDiff(args) {
|
|
|
902
929
|
error('Error getting diff or commits: ' + e.message);
|
|
903
930
|
return;
|
|
904
931
|
}
|
|
905
|
-
|
|
932
|
+
|
|
906
933
|
// Create the prompt for Claude
|
|
907
934
|
const tempDir = `/tmp/claude-analyze-${Date.now()}`;
|
|
908
935
|
fs.mkdirSync(tempDir, { recursive: true });
|
|
909
|
-
|
|
936
|
+
|
|
937
|
+
// Check if subagents should be used
|
|
938
|
+
const useSubagents = process.env.CLAUDE_USE_SUBAGENTS === 'true';
|
|
939
|
+
const subagentModel = process.env.CLAUDE_SUBAGENT_MODEL || 'haiku';
|
|
940
|
+
let subagentBatchSize = parseInt(process.env.CLAUDE_SUBAGENT_BATCH_SIZE || '3');
|
|
941
|
+
// Validate batch size (must be >= 1)
|
|
942
|
+
if (subagentBatchSize < 1) {
|
|
943
|
+
subagentBatchSize = 1;
|
|
944
|
+
}
|
|
945
|
+
const subagentInstruction = useSubagents
|
|
946
|
+
? `\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`
|
|
947
|
+
: '';
|
|
948
|
+
|
|
910
949
|
const promptFile = path.join(tempDir, 'prompt.txt');
|
|
911
950
|
const prompt = `Analyze the following changes. CONTEXT: ${contextDescription}
|
|
912
|
-
|
|
951
|
+
${subagentInstruction}
|
|
913
952
|
Please generate:
|
|
914
953
|
1. A concise and descriptive PR title (maximum 72 characters)
|
|
915
954
|
2. A detailed PR description that includes:
|
|
@@ -943,10 +982,11 @@ ${fullDiff.substring(0, 50000)} ${fullDiff.length > 50000 ? '\n... (truncated di
|
|
|
943
982
|
fs.writeFileSync(promptFile, prompt);
|
|
944
983
|
|
|
945
984
|
info('Sending to Claude for analysis...');
|
|
946
|
-
|
|
985
|
+
const startTime = Date.now();
|
|
986
|
+
|
|
947
987
|
try {
|
|
948
988
|
const response = execSync(`claude < "${promptFile}"`, { encoding: 'utf8', maxBuffer: 1024 * 1024 * 10 });
|
|
949
|
-
|
|
989
|
+
|
|
950
990
|
// Extraer el JSON de la respuesta
|
|
951
991
|
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
952
992
|
if (!jsonMatch) {
|
|
@@ -954,7 +994,7 @@ ${fullDiff.substring(0, 50000)} ${fullDiff.length > 50000 ? '\n... (truncated di
|
|
|
954
994
|
console.log('Complete response:', response);
|
|
955
995
|
return;
|
|
956
996
|
}
|
|
957
|
-
|
|
997
|
+
|
|
958
998
|
let result;
|
|
959
999
|
try {
|
|
960
1000
|
result = JSON.parse(jsonMatch[0]);
|
|
@@ -963,44 +1003,44 @@ ${fullDiff.substring(0, 50000)} ${fullDiff.length > 50000 ? '\n... (truncated di
|
|
|
963
1003
|
console.log('JSON received:', jsonMatch[0]);
|
|
964
1004
|
return;
|
|
965
1005
|
}
|
|
966
|
-
|
|
1006
|
+
|
|
967
1007
|
// Show the results
|
|
968
1008
|
console.log('');
|
|
969
1009
|
console.log('════════════════════════════════════════════════════════════════');
|
|
970
1010
|
console.log(' DIFFERENCES ANALYSIS ');
|
|
971
1011
|
console.log('════════════════════════════════════════════════════════════════');
|
|
972
1012
|
console.log('');
|
|
973
|
-
|
|
1013
|
+
|
|
974
1014
|
console.log(`🔍 ${colors.blue}Context:${colors.reset} ${contextDescription}`);
|
|
975
1015
|
console.log(`📊 ${colors.blue}Changed Files:${colors.reset} ${diffFiles.split('\n').length}`);
|
|
976
1016
|
console.log('');
|
|
977
|
-
|
|
1017
|
+
|
|
978
1018
|
console.log(`📝 ${colors.green}Pull Request Title:${colors.reset}`);
|
|
979
1019
|
console.log(` ${result.prTitle}`);
|
|
980
1020
|
console.log('');
|
|
981
|
-
|
|
1021
|
+
|
|
982
1022
|
console.log(`🌿 ${colors.green}Suggested branch name:${colors.reset}`);
|
|
983
1023
|
console.log(` ${result.suggestedBranchName}`);
|
|
984
1024
|
console.log('');
|
|
985
|
-
|
|
1025
|
+
|
|
986
1026
|
console.log(`📋 ${colors.green}Type of change:${colors.reset} ${result.changeType}`);
|
|
987
|
-
|
|
1027
|
+
|
|
988
1028
|
if (result.breakingChanges) {
|
|
989
1029
|
console.log(`⚠️ ${colors.yellow}Breaking Changes: SÍ${colors.reset}`);
|
|
990
1030
|
}
|
|
991
|
-
|
|
1031
|
+
|
|
992
1032
|
console.log('');
|
|
993
1033
|
console.log(`📄 ${colors.green}Pull Request Description:${colors.reset}`);
|
|
994
1034
|
console.log('───────────────────────────────────────────────────────────────');
|
|
995
1035
|
console.log(result.prDescription);
|
|
996
1036
|
console.log('───────────────────────────────────────────────────────────────');
|
|
997
|
-
|
|
1037
|
+
|
|
998
1038
|
if (result.testingNotes) {
|
|
999
1039
|
console.log('');
|
|
1000
1040
|
console.log(`🧪 ${colors.green}Testing notes:${colors.reset}`);
|
|
1001
1041
|
console.log(result.testingNotes);
|
|
1002
1042
|
}
|
|
1003
|
-
|
|
1043
|
+
|
|
1004
1044
|
// Guardar los resultados en un archivo con contexto
|
|
1005
1045
|
const outputData = {
|
|
1006
1046
|
...result,
|
|
@@ -1012,12 +1052,17 @@ ${fullDiff.substring(0, 50000)} ${fullDiff.length > 50000 ? '\n... (truncated di
|
|
|
1012
1052
|
timestamp: new Date().toISOString()
|
|
1013
1053
|
}
|
|
1014
1054
|
};
|
|
1015
|
-
|
|
1055
|
+
|
|
1016
1056
|
const outputFile = '.claude-pr-analysis.json';
|
|
1017
1057
|
fs.writeFileSync(outputFile, JSON.stringify(outputData, null, 2));
|
|
1058
|
+
|
|
1059
|
+
const elapsed = Date.now() - startTime;
|
|
1060
|
+
const seconds = Math.floor(elapsed / 1000);
|
|
1061
|
+
const ms = elapsed % 1000;
|
|
1018
1062
|
console.log('');
|
|
1063
|
+
console.log(`${colors.blue}⏱️ Analysis completed in ${seconds}.${ms}s${colors.reset}`);
|
|
1019
1064
|
info(`Results saved in ${outputFile}`);
|
|
1020
|
-
|
|
1065
|
+
|
|
1021
1066
|
// Sugerencias contextuales
|
|
1022
1067
|
console.log('');
|
|
1023
1068
|
if (!args[0] && contextDescription.includes('local changes without push')) {
|
|
@@ -1030,9 +1075,9 @@ ${fullDiff.substring(0, 50000)} ${fullDiff.length > 50000 ? '\n... (truncated di
|
|
|
1030
1075
|
console.log(`💡 ${colors.yellow}For renaming your current branch:${colors.reset}`);
|
|
1031
1076
|
console.log(` git branch -m ${result.suggestedBranchName}`);
|
|
1032
1077
|
}
|
|
1033
|
-
|
|
1078
|
+
|
|
1034
1079
|
console.log(`💡 ${colors.yellow}Tip:${colors.reset} Use this information to create your PR on GitHub.`);
|
|
1035
|
-
|
|
1080
|
+
|
|
1036
1081
|
} catch (e) {
|
|
1037
1082
|
error('Error executing Claude: ' + e.message);
|
|
1038
1083
|
} finally {
|
|
@@ -1053,7 +1098,7 @@ function status() {
|
|
|
1053
1098
|
hooks.forEach(hook => {
|
|
1054
1099
|
const enabledPath = `.git/hooks/${hook}`;
|
|
1055
1100
|
const disabledPath = `.git/hooks/${hook}.disabled`;
|
|
1056
|
-
|
|
1101
|
+
|
|
1057
1102
|
if (fs.existsSync(enabledPath)) {
|
|
1058
1103
|
success(`${hook}: enabled`);
|
|
1059
1104
|
} else if (fs.existsSync(disabledPath)) {
|
|
@@ -1084,7 +1129,7 @@ function status() {
|
|
|
1084
1129
|
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
|
1085
1130
|
const claudeIgnores = ['.claude/', 'debug-claude-response.json', '.claude-pr-analysis.json'];
|
|
1086
1131
|
let allPresent = true;
|
|
1087
|
-
|
|
1132
|
+
|
|
1088
1133
|
claudeIgnores.forEach(entry => {
|
|
1089
1134
|
const regex = new RegExp(`^${entry.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm');
|
|
1090
1135
|
if (regex.test(gitignoreContent)) {
|
|
@@ -1094,7 +1139,7 @@ function status() {
|
|
|
1094
1139
|
allPresent = false;
|
|
1095
1140
|
}
|
|
1096
1141
|
});
|
|
1097
|
-
|
|
1142
|
+
|
|
1098
1143
|
if (!allPresent) {
|
|
1099
1144
|
info('\nRun "claude-hooks install" to update .gitignore');
|
|
1100
1145
|
}
|
|
@@ -1116,12 +1161,12 @@ function compareVersions(v1, v2) {
|
|
|
1116
1161
|
// Usar el script compartido para mantener consistencia
|
|
1117
1162
|
const result = execSync(`bash -c 'source "${getTemplatesPath()}/check-version.sh" && compare_versions "${v1}" "${v2}"; echo $?'`, { encoding: 'utf8' }).trim();
|
|
1118
1163
|
const exitCode = parseInt(result);
|
|
1119
|
-
|
|
1164
|
+
|
|
1120
1165
|
// Convertir los códigos de retorno del script bash a valores JS
|
|
1121
1166
|
if (exitCode === 0) return 0; // iguales
|
|
1122
1167
|
if (exitCode === 1) return 1; // v1 > v2
|
|
1123
1168
|
if (exitCode === 2) return -1; // v1 < v2
|
|
1124
|
-
|
|
1169
|
+
|
|
1125
1170
|
// Fallback: comparación simple si el script falla
|
|
1126
1171
|
if (v1 === v2) return 0;
|
|
1127
1172
|
const sorted = [v1, v2].sort((a, b) => {
|
|
@@ -1145,13 +1190,13 @@ function compareVersions(v1, v2) {
|
|
|
1145
1190
|
// Update command - update to the latest version
|
|
1146
1191
|
async function update() {
|
|
1147
1192
|
info('Checking latest available version...');
|
|
1148
|
-
|
|
1193
|
+
|
|
1149
1194
|
try {
|
|
1150
1195
|
const currentVersion = require('../package.json').version;
|
|
1151
1196
|
const latestVersion = await getLatestVersion('claude-git-hooks');
|
|
1152
|
-
|
|
1197
|
+
|
|
1153
1198
|
const comparison = compareVersions(currentVersion, latestVersion);
|
|
1154
|
-
|
|
1199
|
+
|
|
1155
1200
|
if (comparison === 0) {
|
|
1156
1201
|
success(`You already have the latest version installed (${currentVersion})`);
|
|
1157
1202
|
return;
|
|
@@ -1161,20 +1206,20 @@ async function update() {
|
|
|
1161
1206
|
success(`You already have the latest version installed (${currentVersion})`);
|
|
1162
1207
|
return;
|
|
1163
1208
|
}
|
|
1164
|
-
|
|
1209
|
+
|
|
1165
1210
|
info(`Current version: ${currentVersion}`);
|
|
1166
1211
|
info(`Available version: ${latestVersion}`);
|
|
1167
|
-
|
|
1212
|
+
|
|
1168
1213
|
// Actualizar el paquete
|
|
1169
1214
|
info('Updating claude-git-hooks...');
|
|
1170
1215
|
try {
|
|
1171
1216
|
execSync('npm install -g claude-git-hooks@latest', { stdio: 'inherit' });
|
|
1172
1217
|
success(`Successfully updated to version ${latestVersion}`);
|
|
1173
|
-
|
|
1218
|
+
|
|
1174
1219
|
// Reinstall hooks with the new version
|
|
1175
1220
|
info('Reinstalling hooks with the new version...');
|
|
1176
1221
|
await install(['--force']);
|
|
1177
|
-
|
|
1222
|
+
|
|
1178
1223
|
} catch (updateError) {
|
|
1179
1224
|
error('Error updating. Try running: npm install -g claude-git-hooks@latest');
|
|
1180
1225
|
}
|
|
@@ -1241,6 +1286,18 @@ Exclude code from analysis:
|
|
|
1241
1286
|
...excluded code...
|
|
1242
1287
|
// SKIP_ANALYSIS_BLOCK
|
|
1243
1288
|
|
|
1289
|
+
Performance optimization (NEW in v1.5.5):
|
|
1290
|
+
export CLAUDE_USE_SUBAGENTS=true # Enable parallel analysis for 3+ files
|
|
1291
|
+
export CLAUDE_SUBAGENT_MODEL=haiku # Model: haiku (fast), sonnet, opus
|
|
1292
|
+
export CLAUDE_SUBAGENT_BATCH_SIZE=3 # Parallel subagents per batch (default: 3)
|
|
1293
|
+
|
|
1294
|
+
# Batching examples:
|
|
1295
|
+
# BATCH_SIZE=1 with 4 files → 4 sequential batches (1 subagent each)
|
|
1296
|
+
# BATCH_SIZE=3 with 4 files → 2 batches (3 parallel, then 1)
|
|
1297
|
+
# BATCH_SIZE=4 with 4 files → 1 batch (4 parallel subagents)
|
|
1298
|
+
|
|
1299
|
+
# Benefits: Faster for multi-file commits, shows execution time
|
|
1300
|
+
|
|
1244
1301
|
More information: https://github.com/pablorovito/claude-git-hooks
|
|
1245
1302
|
`);
|
|
1246
1303
|
}
|
package/package.json
CHANGED
package/templates/pre-commit
CHANGED
|
@@ -9,6 +9,14 @@ set -e
|
|
|
9
9
|
CLAUDE_CLI="claude"
|
|
10
10
|
TEMP_DIR="/tmp/code-review-$$"
|
|
11
11
|
MAX_FILE_SIZE=100000 # 100KB maximum per file
|
|
12
|
+
USE_SUBAGENTS="${CLAUDE_USE_SUBAGENTS:-false}" # Use subagents for parallel analysis
|
|
13
|
+
SUBAGENT_MODEL="${CLAUDE_SUBAGENT_MODEL:-haiku}" # Model for subagents (haiku/sonnet/opus)
|
|
14
|
+
SUBAGENT_BATCH_SIZE="${CLAUDE_SUBAGENT_BATCH_SIZE:-3}" # Number of parallel subagents per batch
|
|
15
|
+
|
|
16
|
+
# Validate batch size (must be >= 1)
|
|
17
|
+
if [ "$SUBAGENT_BATCH_SIZE" -le 0 ] 2>/dev/null; then
|
|
18
|
+
SUBAGENT_BATCH_SIZE=1
|
|
19
|
+
fi
|
|
12
20
|
|
|
13
21
|
# Colors for output
|
|
14
22
|
RED='\033[0;31m'
|
|
@@ -30,6 +38,16 @@ warning() {
|
|
|
30
38
|
echo -e "${YELLOW}[WARNING]${NC} $1"
|
|
31
39
|
}
|
|
32
40
|
|
|
41
|
+
# Function to show elapsed time
|
|
42
|
+
show_elapsed_time() {
|
|
43
|
+
local start_ms=$1
|
|
44
|
+
local end_ms=$(date +%s%3N 2>/dev/null || echo $(($(date +%s) * 1000)))
|
|
45
|
+
local elapsed=$((end_ms - start_ms))
|
|
46
|
+
local seconds=$((elapsed / 1000))
|
|
47
|
+
local ms=$((elapsed % 1000))
|
|
48
|
+
echo -e "${BLUE}⏱️ Analysis completed in ${seconds}.${ms}s${NC}"
|
|
49
|
+
}
|
|
50
|
+
|
|
33
51
|
# Check version at start (before any analysis)
|
|
34
52
|
# Try to find the check-version.sh script
|
|
35
53
|
CHECK_VERSION_SCRIPT=""
|
|
@@ -166,6 +184,15 @@ cleanup() {
|
|
|
166
184
|
rm -rf "$TEMP_DIR"
|
|
167
185
|
}
|
|
168
186
|
|
|
187
|
+
# Function to inject subagent instruction for parallel analysis
|
|
188
|
+
inject_subagent_instruction() {
|
|
189
|
+
if [ "$USE_SUBAGENTS" = "true" ]; then
|
|
190
|
+
echo ""
|
|
191
|
+
echo "IMPORTANT PARALLEL PROCESSING: If analyzing 3+ files, process them in batches of ${SUBAGENT_BATCH_SIZE}. For EACH batch, create that many subagents in parallel using Task tool (send single message with multiple Task calls). Each subagent analyzes one assigned file following OUTPUT_SCHEMA. After ALL batches complete, consolidate results into SINGLE JSON: (1) merge blockingIssues arrays, (2) merge details arrays, (3) sum issue counts, (4) worst-case metrics (lowest rating), (5) QUALITY_GATE=FAILED if ANY subagent found blockers/criticals, (6) approved=false if any disapproved. Model: ${SUBAGENT_MODEL}. 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)."
|
|
192
|
+
echo ""
|
|
193
|
+
fi
|
|
194
|
+
}
|
|
195
|
+
|
|
169
196
|
# Configure cleanup on exit
|
|
170
197
|
trap cleanup EXIT
|
|
171
198
|
|
|
@@ -250,6 +277,10 @@ cat "$PROMPT_TEMPLATE" > "$PROMPT_FILE"
|
|
|
250
277
|
# Add the guidelines
|
|
251
278
|
echo "=== EVALUATION GUIDELINES ===" >> "$PROMPT_FILE"
|
|
252
279
|
cat "$GUIDELINES_FILE" >> "$PROMPT_FILE"
|
|
280
|
+
|
|
281
|
+
# Inject subagent instruction if enabled
|
|
282
|
+
inject_subagent_instruction >> "$PROMPT_FILE"
|
|
283
|
+
|
|
253
284
|
echo -e "\n\n=== CHANGES TO REVIEW ===\n" >> "$PROMPT_FILE"
|
|
254
285
|
|
|
255
286
|
# Process each Java file
|
|
@@ -300,6 +331,7 @@ log "Sending $FILE_COUNT files for review with Claude..."
|
|
|
300
331
|
|
|
301
332
|
# Send to Claude and capture response
|
|
302
333
|
RESPONSE_FILE="$TEMP_DIR/code_review_response.txt"
|
|
334
|
+
START_TIME=$(date +%s%3N 2>/dev/null || echo $(($(date +%s) * 1000)))
|
|
303
335
|
|
|
304
336
|
# Execute Claude CLI and capture the response
|
|
305
337
|
if $CLAUDE_CLI < "$PROMPT_FILE" > "$RESPONSE_FILE" 2>&1; then
|
|
@@ -402,6 +434,7 @@ if $CLAUDE_CLI < "$PROMPT_FILE" > "$RESPONSE_FILE" 2>&1; then
|
|
|
402
434
|
# Check if commit should be blocked
|
|
403
435
|
if [ "$QUALITY_GATE" = "FAILED" ] || [ "$APPROVED" = "false" ]; then
|
|
404
436
|
echo
|
|
437
|
+
show_elapsed_time "$START_TIME"
|
|
405
438
|
error "❌ Commit blocked due to quality gate failure"
|
|
406
439
|
|
|
407
440
|
# Show blocking issues if they exist
|
|
@@ -420,8 +453,9 @@ if $CLAUDE_CLI < "$PROMPT_FILE" > "$RESPONSE_FILE" 2>&1; then
|
|
|
420
453
|
fi
|
|
421
454
|
|
|
422
455
|
echo
|
|
456
|
+
show_elapsed_time "$START_TIME"
|
|
423
457
|
log "✅ Code analysis completed. Quality gate passed."
|
|
424
|
-
|
|
458
|
+
|
|
425
459
|
else
|
|
426
460
|
error "Error executing Claude CLI"
|
|
427
461
|
error "Check that Claude CLI is configured correctly"
|
|
@@ -10,11 +10,20 @@ CLAUDE_CLI="claude"
|
|
|
10
10
|
TEMP_DIR="/tmp/commit-msg-$$"
|
|
11
11
|
MAX_FILE_SIZE=100000
|
|
12
12
|
AUTO_COMMIT_ENABLED=true
|
|
13
|
+
USE_SUBAGENTS="${CLAUDE_USE_SUBAGENTS:-false}" # Use subagents for parallel analysis
|
|
14
|
+
SUBAGENT_MODEL="${CLAUDE_SUBAGENT_MODEL:-haiku}" # Model for subagents (haiku/sonnet/opus)
|
|
15
|
+
SUBAGENT_BATCH_SIZE="${CLAUDE_SUBAGENT_BATCH_SIZE:-3}" # Number of parallel subagents per batch
|
|
16
|
+
|
|
17
|
+
# Validate batch size (must be >= 1)
|
|
18
|
+
if [ "$SUBAGENT_BATCH_SIZE" -le 0 ] 2>/dev/null; then
|
|
19
|
+
SUBAGENT_BATCH_SIZE=1
|
|
20
|
+
fi
|
|
13
21
|
|
|
14
22
|
# Colors for output
|
|
15
23
|
RED='\033[0;31m'
|
|
16
24
|
GREEN='\033[0;32m'
|
|
17
25
|
YELLOW='\033[1;33m'
|
|
26
|
+
BLUE='\033[0;34m'
|
|
18
27
|
NC='\033[0m'
|
|
19
28
|
|
|
20
29
|
# Function for logging
|
|
@@ -26,10 +35,30 @@ warning() {
|
|
|
26
35
|
printf "${YELLOW}[WARNING]${NC} %s\n" "$1" >&2
|
|
27
36
|
}
|
|
28
37
|
|
|
38
|
+
# Function to show elapsed time
|
|
39
|
+
show_elapsed_time() {
|
|
40
|
+
local start_ms=$1
|
|
41
|
+
local end_ms=$(date +%s%3N 2>/dev/null || echo $(($(date +%s) * 1000)))
|
|
42
|
+
local elapsed=$((end_ms - start_ms))
|
|
43
|
+
local seconds=$((elapsed / 1000))
|
|
44
|
+
local ms=$((elapsed % 1000))
|
|
45
|
+
printf "${BLUE}⏱️ Message generation completed in ${seconds}.${ms}s${NC}\n" >&2
|
|
46
|
+
}
|
|
47
|
+
|
|
29
48
|
# Function to clean temporary files
|
|
30
49
|
cleanup() {
|
|
31
50
|
rm -rf "$TEMP_DIR"
|
|
32
51
|
}
|
|
52
|
+
|
|
53
|
+
# Function to inject subagent instruction for parallel analysis
|
|
54
|
+
inject_subagent_instruction() {
|
|
55
|
+
if [ "$USE_SUBAGENTS" = "true" ]; then
|
|
56
|
+
echo ""
|
|
57
|
+
echo "IMPORTANT PARALLEL PROCESSING: If analyzing 3+ files, process them in batches of ${SUBAGENT_BATCH_SIZE}. 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 summary. After ALL batches complete, consolidate into SINGLE JSON with ONE commit message describing all changes. Determine type based on predominant change. Model: ${SUBAGENT_MODEL}. 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)."
|
|
58
|
+
echo ""
|
|
59
|
+
fi
|
|
60
|
+
}
|
|
61
|
+
|
|
33
62
|
trap cleanup EXIT
|
|
34
63
|
|
|
35
64
|
# Hook arguments
|
|
@@ -76,6 +105,9 @@ Respond ONLY with a valid JSON:
|
|
|
76
105
|
CHANGES TO ANALYZE:
|
|
77
106
|
EOF
|
|
78
107
|
|
|
108
|
+
# Inject subagent instruction if enabled
|
|
109
|
+
inject_subagent_instruction >> "$PROMPT_FILE"
|
|
110
|
+
|
|
79
111
|
# Get staged files
|
|
80
112
|
ALL_STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null || echo "")
|
|
81
113
|
|
|
@@ -105,6 +137,7 @@ done
|
|
|
105
137
|
|
|
106
138
|
RESPONSE_FILE="$TEMP_DIR/commit_msg_response.txt"
|
|
107
139
|
log "Generating commit message with Claude..."
|
|
140
|
+
START_TIME=$(date +%s%3N 2>/dev/null || echo $(($(date +%s) * 1000)))
|
|
108
141
|
|
|
109
142
|
if $CLAUDE_CLI < "$PROMPT_FILE" > "$RESPONSE_FILE" 2>&1; then
|
|
110
143
|
JSON_MSG=$(sed -n '/^{/,/^}/p' "$RESPONSE_FILE" | head -n 1000)
|
|
@@ -127,6 +160,7 @@ if $CLAUDE_CLI < "$PROMPT_FILE" > "$RESPONSE_FILE" 2>&1; then
|
|
|
127
160
|
fi
|
|
128
161
|
|
|
129
162
|
printf "%s\n" "$FULL_MESSAGE" > "$COMMIT_MSG_FILE"
|
|
163
|
+
show_elapsed_time "$START_TIME"
|
|
130
164
|
log "📝 Message generated: $(echo "$FULL_MESSAGE" | head -n 1)"
|
|
131
165
|
exit 0
|
|
132
166
|
fi
|