claude-git-hooks 1.5.4 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +89 -1
- package/README.md +130 -35
- package/bin/claude-hooks +253 -287
- package/lib/hooks/pre-commit.js +335 -0
- package/lib/hooks/prepare-commit-msg.js +283 -0
- package/lib/utils/claude-client.js +373 -0
- package/lib/utils/file-operations.js +409 -0
- package/lib/utils/git-operations.js +341 -0
- package/lib/utils/logger.js +141 -0
- package/lib/utils/prompt-builder.js +283 -0
- package/lib/utils/resolution-prompt.js +291 -0
- package/package.json +52 -40
- package/templates/pre-commit +58 -411
- package/templates/prepare-commit-msg +62 -118
package/bin/claude-hooks
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
import { execSync, spawn } from 'child_process';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import os from 'os';
|
|
7
|
+
import readline from 'readline';
|
|
8
|
+
import https from 'https';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { dirname } from 'path';
|
|
11
|
+
import { executeClaude, extractJSON } from '../lib/utils/claude-client.js';
|
|
12
|
+
|
|
13
|
+
// Why: ES6 modules don't have __dirname, need to recreate it
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
|
|
17
|
+
// Helper to read package.json
|
|
18
|
+
// Why: ES6 modules can't use require() for JSON files
|
|
19
|
+
const getPackageJson = () => {
|
|
20
|
+
const packagePath = path.join(__dirname, '..', 'package.json');
|
|
21
|
+
return JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
22
|
+
};
|
|
9
23
|
|
|
10
24
|
// Function to get the latest version from NPM
|
|
11
25
|
function getLatestVersion(packageName) {
|
|
@@ -40,26 +54,26 @@ function getLatestVersion(packageName) {
|
|
|
40
54
|
// Function to check version (used by hooks)
|
|
41
55
|
async function checkVersionAndPromptUpdate() {
|
|
42
56
|
try {
|
|
43
|
-
const currentVersion =
|
|
57
|
+
const currentVersion = getPackageJson().version;
|
|
44
58
|
const latestVersion = await getLatestVersion('claude-git-hooks');
|
|
45
|
-
|
|
59
|
+
|
|
46
60
|
if (currentVersion === latestVersion) {
|
|
47
61
|
return true; // Already updated
|
|
48
62
|
}
|
|
49
|
-
|
|
63
|
+
|
|
50
64
|
console.log('');
|
|
51
65
|
warning(`New version available: ${latestVersion} (current: ${currentVersion})`);
|
|
52
|
-
|
|
66
|
+
|
|
53
67
|
// Interactive prompt compatible with all consoles
|
|
54
68
|
const rl = readline.createInterface({
|
|
55
69
|
input: process.stdin,
|
|
56
70
|
output: process.stdout
|
|
57
71
|
});
|
|
58
|
-
|
|
72
|
+
|
|
59
73
|
return new Promise((resolve) => {
|
|
60
74
|
rl.question('Do you want to update now? (y/n): ', (answer) => {
|
|
61
75
|
rl.close();
|
|
62
|
-
|
|
76
|
+
|
|
63
77
|
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
|
64
78
|
info('Updating claude-git-hooks...');
|
|
65
79
|
try {
|
|
@@ -82,11 +96,6 @@ async function checkVersionAndPromptUpdate() {
|
|
|
82
96
|
}
|
|
83
97
|
}
|
|
84
98
|
|
|
85
|
-
// Export for use in hooks
|
|
86
|
-
if (typeof module !== 'undefined' && module.exports) {
|
|
87
|
-
module.exports = { checkVersionAndPromptUpdate };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
99
|
// Colors for output
|
|
91
100
|
const colors = {
|
|
92
101
|
reset: '\x1b[0m',
|
|
@@ -124,7 +133,7 @@ function readPassword(prompt) {
|
|
|
124
133
|
input: process.stdin,
|
|
125
134
|
output: process.stdout
|
|
126
135
|
});
|
|
127
|
-
|
|
136
|
+
|
|
128
137
|
// Disable echo
|
|
129
138
|
rl.stdoutMuted = true;
|
|
130
139
|
rl._writeToOutput = function _writeToOutput(stringToWrite) {
|
|
@@ -133,7 +142,7 @@ function readPassword(prompt) {
|
|
|
133
142
|
else
|
|
134
143
|
rl.output.write(stringToWrite);
|
|
135
144
|
};
|
|
136
|
-
|
|
145
|
+
|
|
137
146
|
rl.question(prompt, (password) => {
|
|
138
147
|
rl.close();
|
|
139
148
|
console.log(); // New line
|
|
@@ -142,45 +151,6 @@ function readPassword(prompt) {
|
|
|
142
151
|
});
|
|
143
152
|
}
|
|
144
153
|
|
|
145
|
-
// Check if sudo password is correct
|
|
146
|
-
function testSudoPassword(password) {
|
|
147
|
-
try {
|
|
148
|
-
execSync('echo "' + password + '" | sudo -S true', {
|
|
149
|
-
stdio: 'ignore',
|
|
150
|
-
timeout: 5000
|
|
151
|
-
});
|
|
152
|
-
return true;
|
|
153
|
-
} catch (e) {
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Install package with automatic sudo
|
|
159
|
-
function installPackage(packageName, sudoPassword = null) {
|
|
160
|
-
try {
|
|
161
|
-
if (sudoPassword) {
|
|
162
|
-
if (os.platform() === 'linux') {
|
|
163
|
-
execSync(`echo "${sudoPassword}" | sudo -S apt-get update && echo "${sudoPassword}" | sudo -S apt-get install -y ${packageName}`, {
|
|
164
|
-
stdio: 'inherit'
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
} else {
|
|
168
|
-
if (os.platform() === 'linux') {
|
|
169
|
-
execSync(`sudo apt-get update && sudo apt-get install -y ${packageName}`, {
|
|
170
|
-
stdio: 'inherit'
|
|
171
|
-
});
|
|
172
|
-
} else if (os.platform() === 'darwin') {
|
|
173
|
-
execSync(`brew install ${packageName}`, {
|
|
174
|
-
stdio: 'inherit'
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return true;
|
|
179
|
-
} catch (e) {
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
154
|
// Entertainment system
|
|
185
155
|
class Entertainment {
|
|
186
156
|
static jokes = [
|
|
@@ -231,26 +201,26 @@ class Entertainment {
|
|
|
231
201
|
// Get first joke from API without blocking
|
|
232
202
|
this.getJoke().then(joke => {
|
|
233
203
|
if (!isFinished) currentJoke = joke;
|
|
234
|
-
}).catch(() => {}); // If it fails, keep the local one
|
|
204
|
+
}).catch(() => { }); // If it fails, keep the local one
|
|
235
205
|
|
|
236
206
|
// Hide cursor
|
|
237
207
|
process.stdout.write('\x1B[?25l');
|
|
238
|
-
|
|
208
|
+
|
|
239
209
|
// Reserve space for the 3 lines
|
|
240
210
|
process.stdout.write('\n\n\n');
|
|
241
|
-
|
|
211
|
+
|
|
242
212
|
const interval = setInterval(() => {
|
|
243
213
|
if (isFinished) {
|
|
244
214
|
clearInterval(interval);
|
|
245
215
|
return;
|
|
246
216
|
}
|
|
247
|
-
|
|
217
|
+
|
|
248
218
|
spinnerIndex++;
|
|
249
|
-
|
|
219
|
+
|
|
250
220
|
// Update countdown every second (10 iterations of 100ms)
|
|
251
221
|
if (spinnerIndex % 10 === 0) {
|
|
252
222
|
jokeCountdown--;
|
|
253
|
-
|
|
223
|
+
|
|
254
224
|
// Refresh joke every 10 seconds
|
|
255
225
|
if (jokeCountdown <= 0) {
|
|
256
226
|
this.getJoke().then(joke => {
|
|
@@ -263,19 +233,19 @@ class Entertainment {
|
|
|
263
233
|
jokeCountdown = 10;
|
|
264
234
|
}
|
|
265
235
|
}
|
|
266
|
-
|
|
236
|
+
|
|
267
237
|
// Always go back exactly 3 lines up
|
|
268
238
|
process.stdout.write('\x1B[3A');
|
|
269
|
-
|
|
239
|
+
|
|
270
240
|
// Render the 3 lines from the beginning
|
|
271
241
|
const spinner = spinners[spinnerIndex % spinners.length];
|
|
272
|
-
|
|
242
|
+
|
|
273
243
|
// Line 1: Spinner
|
|
274
244
|
process.stdout.write('\r\x1B[2K' + `${colors.yellow}${spinner} ${message}${colors.reset}\n`);
|
|
275
|
-
|
|
245
|
+
|
|
276
246
|
// Line 2: Joke
|
|
277
247
|
process.stdout.write('\r\x1B[2K' + `${colors.green}🎭 ${currentJoke}${colors.reset}\n`);
|
|
278
|
-
|
|
248
|
+
|
|
279
249
|
// Line 3: Countdown
|
|
280
250
|
process.stdout.write('\r\x1B[2K' + `${colors.yellow}⏱️ Next joke in: ${jokeCountdown}s${colors.reset}\n`);
|
|
281
251
|
}, 100);
|
|
@@ -284,7 +254,7 @@ class Entertainment {
|
|
|
284
254
|
const result = await promise;
|
|
285
255
|
isFinished = true;
|
|
286
256
|
clearInterval(interval);
|
|
287
|
-
|
|
257
|
+
|
|
288
258
|
// Clean exactly 3 lines completely
|
|
289
259
|
process.stdout.write('\x1B[3A'); // Go up 3 lines
|
|
290
260
|
process.stdout.write('\r\x1B[2K'); // Clean line 1
|
|
@@ -292,15 +262,15 @@ class Entertainment {
|
|
|
292
262
|
process.stdout.write('\n\r\x1B[2K'); // Go down and clean line 3
|
|
293
263
|
process.stdout.write('\x1B[2A'); // Go up 2 lines to end up on the first
|
|
294
264
|
process.stdout.write('\r'); // Go to beginning of line
|
|
295
|
-
|
|
265
|
+
|
|
296
266
|
// Show cursor
|
|
297
267
|
process.stdout.write('\x1B[?25h');
|
|
298
|
-
|
|
268
|
+
|
|
299
269
|
return result;
|
|
300
270
|
} catch (error) {
|
|
301
271
|
isFinished = true;
|
|
302
272
|
clearInterval(interval);
|
|
303
|
-
|
|
273
|
+
|
|
304
274
|
// Clean exactly 3 lines completely
|
|
305
275
|
process.stdout.write('\x1B[3A'); // Go up 3 lines
|
|
306
276
|
process.stdout.write('\r\x1B[2K'); // Clean line 1
|
|
@@ -308,21 +278,42 @@ class Entertainment {
|
|
|
308
278
|
process.stdout.write('\n\r\x1B[2K'); // Go down and clean line 3
|
|
309
279
|
process.stdout.write('\x1B[2A'); // Go up 2 lines to end up on the first
|
|
310
280
|
process.stdout.write('\r'); // Go to beginning of line
|
|
311
|
-
|
|
281
|
+
|
|
312
282
|
// Show cursor
|
|
313
283
|
process.stdout.write('\x1B[?25h');
|
|
314
|
-
|
|
284
|
+
|
|
315
285
|
throw error;
|
|
316
286
|
}
|
|
317
287
|
}
|
|
318
288
|
}
|
|
319
289
|
|
|
320
|
-
// Check if we are in a git repository
|
|
290
|
+
// Check if we are in a git repository (including worktrees created in PowerShell)
|
|
321
291
|
function checkGitRepo() {
|
|
322
292
|
try {
|
|
323
293
|
execSync('git rev-parse --git-dir', { stdio: 'ignore' });
|
|
324
294
|
return true;
|
|
325
295
|
} catch (e) {
|
|
296
|
+
// Try to detect worktree created in PowerShell
|
|
297
|
+
try {
|
|
298
|
+
if (fs.existsSync('.git')) {
|
|
299
|
+
const gitContent = fs.readFileSync('.git', 'utf8').trim();
|
|
300
|
+
// Check if it's a worktree pointer (gitdir: ...)
|
|
301
|
+
if (gitContent.startsWith('gitdir:')) {
|
|
302
|
+
let gitdir = gitContent.substring(8).trim();
|
|
303
|
+
// Convert Windows path to WSL if needed (C:\ -> /mnt/c/)
|
|
304
|
+
if (/^[A-Za-z]:/.test(gitdir)) {
|
|
305
|
+
gitdir = gitdir.replace(/^([A-Za-z]):/, (_, drive) => `/mnt/${drive.toLowerCase()}`);
|
|
306
|
+
gitdir = gitdir.replace(/\\/g, '/');
|
|
307
|
+
}
|
|
308
|
+
// Verify the gitdir exists
|
|
309
|
+
if (fs.existsSync(gitdir)) {
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
} catch (worktreeError) {
|
|
315
|
+
// Ignore worktree detection errors
|
|
316
|
+
}
|
|
326
317
|
return false;
|
|
327
318
|
}
|
|
328
319
|
}
|
|
@@ -340,32 +331,16 @@ async function install(args) {
|
|
|
340
331
|
|
|
341
332
|
const isForce = args.includes('--force');
|
|
342
333
|
const skipAuth = args.includes('--skip-auth');
|
|
343
|
-
|
|
334
|
+
|
|
344
335
|
if (isForce) {
|
|
345
336
|
info('Installing Claude Git Hooks (force mode)...');
|
|
346
337
|
} else {
|
|
347
338
|
info('Installing Claude Git Hooks...');
|
|
348
339
|
}
|
|
349
340
|
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const needsInstall = await checkIfInstallationNeeded();
|
|
354
|
-
if (needsInstall) {
|
|
355
|
-
info('Sudo access is needed for automatic dependency installation, please enter password');
|
|
356
|
-
sudoPassword = await readPassword('Enter your Ubuntu password for sudo: ');
|
|
357
|
-
|
|
358
|
-
if (sudoPassword && !testSudoPassword(sudoPassword)) {
|
|
359
|
-
warning('Incorrect password. Continuing without automatic installation.');
|
|
360
|
-
sudoPassword = null;
|
|
361
|
-
} else if (sudoPassword) {
|
|
362
|
-
success('Password verified. Proceeding with automatic installation.');
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Check dependencies with automatic installation
|
|
368
|
-
await checkAndInstallDependencies(sudoPassword, skipAuth);
|
|
341
|
+
// v2.0.0+: No sudo needed (pure Node.js, no system packages required)
|
|
342
|
+
// Check dependencies
|
|
343
|
+
await checkAndInstallDependencies(null, skipAuth);
|
|
369
344
|
|
|
370
345
|
const templatesPath = getTemplatesPath();
|
|
371
346
|
const hooksPath = '.git/hooks';
|
|
@@ -375,32 +350,41 @@ async function install(args) {
|
|
|
375
350
|
fs.mkdirSync(hooksPath, { recursive: true });
|
|
376
351
|
}
|
|
377
352
|
|
|
353
|
+
// Helper function to copy file with LF line endings
|
|
354
|
+
// Why: Bash scripts must have LF (Unix) line endings, not CRLF (Windows)
|
|
355
|
+
const copyWithLF = (sourcePath, destPath) => {
|
|
356
|
+
let content = fs.readFileSync(sourcePath, 'utf8');
|
|
357
|
+
// Convert CRLF to LF
|
|
358
|
+
content = content.replace(/\r\n/g, '\n');
|
|
359
|
+
fs.writeFileSync(destPath, content, 'utf8');
|
|
360
|
+
};
|
|
361
|
+
|
|
378
362
|
// Hooks to install
|
|
379
363
|
const hooks = ['pre-commit', 'prepare-commit-msg'];
|
|
380
|
-
|
|
364
|
+
|
|
381
365
|
hooks.forEach(hook => {
|
|
382
366
|
const sourcePath = path.join(templatesPath, hook);
|
|
383
367
|
const destPath = path.join(hooksPath, hook);
|
|
384
|
-
|
|
368
|
+
|
|
385
369
|
// Make backup if it exists
|
|
386
370
|
if (fs.existsSync(destPath)) {
|
|
387
371
|
const backupPath = `${destPath}.backup.${Date.now()}`;
|
|
388
372
|
fs.copyFileSync(destPath, backupPath);
|
|
389
373
|
info(`Backup created: ${backupPath}`);
|
|
390
374
|
}
|
|
391
|
-
|
|
392
|
-
// Copy hook
|
|
393
|
-
|
|
375
|
+
|
|
376
|
+
// Copy hook with LF line endings (critical for bash)
|
|
377
|
+
copyWithLF(sourcePath, destPath);
|
|
394
378
|
fs.chmodSync(destPath, '755');
|
|
395
379
|
success(`${hook} installed`);
|
|
396
380
|
});
|
|
397
381
|
|
|
398
|
-
// Copy version verification script
|
|
382
|
+
// Copy version verification script with LF line endings
|
|
399
383
|
const checkVersionSource = path.join(templatesPath, 'check-version.sh');
|
|
400
384
|
const checkVersionDest = path.join(hooksPath, 'check-version.sh');
|
|
401
|
-
|
|
385
|
+
|
|
402
386
|
if (fs.existsSync(checkVersionSource)) {
|
|
403
|
-
|
|
387
|
+
copyWithLF(checkVersionSource, checkVersionDest);
|
|
404
388
|
fs.chmodSync(checkVersionDest, '755');
|
|
405
389
|
success('Version verification script installed');
|
|
406
390
|
}
|
|
@@ -418,11 +402,11 @@ async function install(args) {
|
|
|
418
402
|
'CLAUDE_ANALYSIS_PROMPT_SONAR.md',
|
|
419
403
|
'CLAUDE_RESOLUTION_PROMPT.md'
|
|
420
404
|
];
|
|
421
|
-
|
|
405
|
+
|
|
422
406
|
claudeFiles.forEach(file => {
|
|
423
407
|
const destPath = path.join(claudeDir, file);
|
|
424
408
|
const sourcePath = path.join(templatesPath, file);
|
|
425
|
-
|
|
409
|
+
|
|
426
410
|
// In force mode or if it doesn't exist, copy the file
|
|
427
411
|
if (isForce || !fs.existsSync(destPath)) {
|
|
428
412
|
if (fs.existsSync(sourcePath)) {
|
|
@@ -450,13 +434,19 @@ async function install(args) {
|
|
|
450
434
|
console.log(' // SKIP_ANALYSIS_BLOCK # Exclude block until finding another equal one');
|
|
451
435
|
console.log(' ...excluded code...');
|
|
452
436
|
console.log(' // SKIP_ANALYSIS_BLOCK');
|
|
437
|
+
console.log('\nNEW: Parallel analysis for faster multi-file commits:');
|
|
438
|
+
console.log(' export CLAUDE_USE_SUBAGENTS=true # Enable subagents');
|
|
439
|
+
console.log(' export CLAUDE_SUBAGENT_MODEL=haiku # haiku/sonnet/opus');
|
|
440
|
+
console.log(' export CLAUDE_SUBAGENT_BATCH_SIZE=3 # Parallel per batch (default: 3)');
|
|
441
|
+
console.log(' # Example: 4 files, BATCH_SIZE=1 → 4 sequential batches');
|
|
442
|
+
console.log(' # Example: 4 files, BATCH_SIZE=3 → 2 batches (3 parallel + 1)');
|
|
453
443
|
console.log('\nFor more options: claude-hooks --help');
|
|
454
444
|
}
|
|
455
445
|
|
|
456
446
|
// Check complete dependencies (like setup-wsl.sh)
|
|
457
447
|
async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false) {
|
|
458
448
|
info('Checking system dependencies...');
|
|
459
|
-
|
|
449
|
+
|
|
460
450
|
// Check Node.js
|
|
461
451
|
try {
|
|
462
452
|
const nodeVersion = execSync('node --version', { encoding: 'utf8' }).trim();
|
|
@@ -464,7 +454,7 @@ async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false
|
|
|
464
454
|
} catch (e) {
|
|
465
455
|
error('Node.js is not installed. Install Node.js and try again.');
|
|
466
456
|
}
|
|
467
|
-
|
|
457
|
+
|
|
468
458
|
// Check npm
|
|
469
459
|
try {
|
|
470
460
|
const npmVersion = execSync('npm --version', { encoding: 'utf8' }).trim();
|
|
@@ -472,43 +462,9 @@ async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false
|
|
|
472
462
|
} catch (e) {
|
|
473
463
|
error('npm is not installed.');
|
|
474
464
|
}
|
|
475
|
-
|
|
476
|
-
//
|
|
477
|
-
|
|
478
|
-
const jqVersion = execSync('jq --version', { encoding: 'utf8' }).trim();
|
|
479
|
-
success(`jq ${jqVersion}`);
|
|
480
|
-
} catch (e) {
|
|
481
|
-
warning('jq is not installed. Installing...');
|
|
482
|
-
if (installPackage('jq', sudoPassword)) {
|
|
483
|
-
success('jq installed successfully');
|
|
484
|
-
} else {
|
|
485
|
-
warning('Could not install jq automatically');
|
|
486
|
-
if (os.platform() === 'linux') {
|
|
487
|
-
console.log('Install it manually with: sudo apt install jq');
|
|
488
|
-
} else if (os.platform() === 'darwin') {
|
|
489
|
-
console.log('Install it manually with: brew install jq');
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// Check and install curl
|
|
495
|
-
try {
|
|
496
|
-
const curlVersion = execSync('curl --version', { encoding: 'utf8' }).split('\n')[0];
|
|
497
|
-
success(`curl ${curlVersion.split(' ')[1]}`);
|
|
498
|
-
} catch (e) {
|
|
499
|
-
warning('curl is not installed. Installing...');
|
|
500
|
-
if (installPackage('curl', sudoPassword)) {
|
|
501
|
-
success('curl installed successfully');
|
|
502
|
-
} else {
|
|
503
|
-
warning('Could not install curl automatically');
|
|
504
|
-
if (os.platform() === 'linux') {
|
|
505
|
-
console.log('Install it manually with: sudo apt install curl');
|
|
506
|
-
} else if (os.platform() === 'darwin') {
|
|
507
|
-
console.log('Install it manually with: brew install curl');
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
|
|
465
|
+
|
|
466
|
+
// v2.0.0+: jq and curl are no longer needed (pure Node.js implementation)
|
|
467
|
+
|
|
512
468
|
// Check Git
|
|
513
469
|
try {
|
|
514
470
|
const gitVersion = execSync('git --version', { encoding: 'utf8' }).trim();
|
|
@@ -516,95 +472,98 @@ async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false
|
|
|
516
472
|
} catch (e) {
|
|
517
473
|
error('Git is not installed. Install Git and try again.');
|
|
518
474
|
}
|
|
519
|
-
|
|
520
|
-
//
|
|
521
|
-
|
|
522
|
-
const missingTools = [];
|
|
523
|
-
|
|
524
|
-
unixTools.forEach(tool => {
|
|
525
|
-
try {
|
|
526
|
-
execSync(`which ${tool}`, { stdio: 'ignore' });
|
|
527
|
-
} catch (e) {
|
|
528
|
-
missingTools.push(tool);
|
|
529
|
-
}
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
if (missingTools.length === 0) {
|
|
533
|
-
success('Standard Unix tools verified');
|
|
534
|
-
} else {
|
|
535
|
-
error(`Missing standard Unix tools: ${missingTools.join(', ')}. Retry installation in an Ubuntu console`);
|
|
536
|
-
}
|
|
537
|
-
|
|
475
|
+
|
|
476
|
+
// v2.0.0+: Unix tools (sed, awk, grep, etc.) no longer needed (pure Node.js implementation)
|
|
477
|
+
|
|
538
478
|
// Check and install Claude CLI
|
|
539
479
|
await checkAndInstallClaude();
|
|
540
|
-
|
|
480
|
+
|
|
541
481
|
// Check Claude authentication (if not skipped)
|
|
542
482
|
if (!skipAuth) {
|
|
543
483
|
await checkClaudeAuth();
|
|
544
484
|
} else {
|
|
545
485
|
warning('Skipping Claude authentication verification (--skip-auth)');
|
|
546
486
|
}
|
|
547
|
-
|
|
487
|
+
|
|
548
488
|
// Clear password from memory
|
|
549
489
|
sudoPassword = null;
|
|
550
490
|
}
|
|
551
491
|
|
|
492
|
+
// Detect if running on Windows
|
|
493
|
+
// Why: Need to use 'wsl claude' instead of 'claude' on Windows
|
|
494
|
+
function isWindows() {
|
|
495
|
+
return os.platform() === 'win32' || process.env.OS === 'Windows_NT';
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Get Claude command based on platform
|
|
499
|
+
// Why: On Windows, Claude CLI runs in WSL, so we need 'wsl claude'
|
|
500
|
+
function getClaudeCommand() {
|
|
501
|
+
return isWindows() ? 'wsl claude' : 'claude';
|
|
502
|
+
}
|
|
503
|
+
|
|
552
504
|
// Check if we need to install dependencies
|
|
553
505
|
async function checkIfInstallationNeeded() {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
try {
|
|
558
|
-
execSync(`which ${dep}`, { stdio: 'ignore' });
|
|
559
|
-
} catch (e) {
|
|
560
|
-
return true; // Needs installation
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// Verificar Claude CLI
|
|
506
|
+
// v2.0.0+: Only check Claude CLI (jq and curl no longer needed)
|
|
507
|
+
const claudeCmd = getClaudeCommand();
|
|
508
|
+
|
|
565
509
|
try {
|
|
566
|
-
execSync(
|
|
510
|
+
execSync(`${claudeCmd} --version`, { stdio: 'ignore' });
|
|
567
511
|
} catch (e) {
|
|
568
512
|
return true; // Needs Claude installation
|
|
569
513
|
}
|
|
570
|
-
|
|
514
|
+
|
|
571
515
|
return false;
|
|
572
516
|
}
|
|
573
517
|
|
|
574
|
-
// Check
|
|
518
|
+
// Check Claude CLI availability
|
|
575
519
|
async function checkAndInstallClaude() {
|
|
520
|
+
const claudeCmd = getClaudeCommand();
|
|
521
|
+
const platform = isWindows() ? 'Windows (via WSL)' : os.platform();
|
|
522
|
+
|
|
576
523
|
try {
|
|
577
|
-
execSync(
|
|
578
|
-
success(
|
|
524
|
+
execSync(`${claudeCmd} --version`, { stdio: 'ignore' });
|
|
525
|
+
success(`Claude CLI detected (${platform})`);
|
|
579
526
|
} catch (e) {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
527
|
+
error(`Claude CLI not detected on ${platform}`);
|
|
528
|
+
|
|
529
|
+
if (isWindows()) {
|
|
530
|
+
console.log('\n⚠️ On Windows, Claude CLI must be installed in WSL:');
|
|
531
|
+
console.log('1. Open WSL terminal (wsl or Ubuntu from Start Menu)');
|
|
532
|
+
console.log('2. Follow installation at: https://docs.anthropic.com/claude/docs/claude-cli');
|
|
533
|
+
console.log('3. Verify with: wsl claude --version');
|
|
534
|
+
} else {
|
|
535
|
+
console.log('\nClaude CLI installation: https://docs.anthropic.com/claude/docs/claude-cli');
|
|
586
536
|
}
|
|
537
|
+
|
|
538
|
+
console.log('\nAfter installation, run: claude-hooks install --force');
|
|
539
|
+
process.exit(1);
|
|
587
540
|
}
|
|
588
541
|
}
|
|
589
542
|
|
|
590
543
|
// Check Claude authentication with entertainment
|
|
591
544
|
async function checkClaudeAuth() {
|
|
592
545
|
info('Checking Claude authentication...');
|
|
593
|
-
|
|
546
|
+
|
|
547
|
+
// Get correct Claude command for platform
|
|
548
|
+
const claudeCmd = getClaudeCommand();
|
|
549
|
+
const cmdParts = claudeCmd.split(' ');
|
|
550
|
+
const command = cmdParts[0];
|
|
551
|
+
const args = [...cmdParts.slice(1), 'auth', 'status'];
|
|
552
|
+
|
|
594
553
|
// Use spawn to not block, but with stdio: 'ignore' like the original
|
|
595
554
|
const authPromise = new Promise((resolve, reject) => {
|
|
596
|
-
const child = spawn(
|
|
555
|
+
const child = spawn(command, args, {
|
|
597
556
|
stdio: 'ignore', // Igual que el original
|
|
598
557
|
detached: false,
|
|
599
558
|
windowsHide: true
|
|
600
559
|
});
|
|
601
|
-
|
|
560
|
+
|
|
602
561
|
// Manual timeout since spawn doesn't have native timeout
|
|
603
562
|
const timeout = setTimeout(() => {
|
|
604
563
|
child.kill();
|
|
605
564
|
reject(new Error('timeout'));
|
|
606
565
|
}, 120000); // 2 minutos
|
|
607
|
-
|
|
566
|
+
|
|
608
567
|
child.on('exit', (code) => {
|
|
609
568
|
clearTimeout(timeout);
|
|
610
569
|
if (code === 0) {
|
|
@@ -613,7 +572,7 @@ async function checkClaudeAuth() {
|
|
|
613
572
|
reject(new Error('not_authenticated'));
|
|
614
573
|
}
|
|
615
574
|
});
|
|
616
|
-
|
|
575
|
+
|
|
617
576
|
child.on('error', (err) => {
|
|
618
577
|
clearTimeout(timeout);
|
|
619
578
|
reject(err);
|
|
@@ -633,7 +592,7 @@ async function checkClaudeAuth() {
|
|
|
633
592
|
// Update .gitignore with Claude entries
|
|
634
593
|
function updateGitignore() {
|
|
635
594
|
info('Updating .gitignore...');
|
|
636
|
-
|
|
595
|
+
|
|
637
596
|
const gitignorePath = '.gitignore';
|
|
638
597
|
const claudeEntries = [
|
|
639
598
|
'# Claude Git Hooks',
|
|
@@ -642,16 +601,16 @@ function updateGitignore() {
|
|
|
642
601
|
'claude_resolution_prompt.md',
|
|
643
602
|
'.claude-pr-analysis.json',
|
|
644
603
|
];
|
|
645
|
-
|
|
604
|
+
|
|
646
605
|
let gitignoreContent = '';
|
|
647
606
|
let fileExists = false;
|
|
648
|
-
|
|
607
|
+
|
|
649
608
|
// Read existing .gitignore if it exists
|
|
650
609
|
if (fs.existsSync(gitignorePath)) {
|
|
651
610
|
gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
|
652
611
|
fileExists = true;
|
|
653
612
|
}
|
|
654
|
-
|
|
613
|
+
|
|
655
614
|
// Check which entries are missing
|
|
656
615
|
const missingEntries = [];
|
|
657
616
|
claudeEntries.forEach(entry => {
|
|
@@ -668,31 +627,31 @@ function updateGitignore() {
|
|
|
668
627
|
}
|
|
669
628
|
}
|
|
670
629
|
});
|
|
671
|
-
|
|
630
|
+
|
|
672
631
|
// If there are missing entries, add them
|
|
673
632
|
if (missingEntries.length > 0) {
|
|
674
633
|
// Ensure there's a newline at the end if the file exists and is not empty
|
|
675
634
|
if (fileExists && gitignoreContent.length > 0 && !gitignoreContent.endsWith('\n')) {
|
|
676
635
|
gitignoreContent += '\n';
|
|
677
636
|
}
|
|
678
|
-
|
|
637
|
+
|
|
679
638
|
// If the file is not empty, add a blank line before
|
|
680
639
|
if (gitignoreContent.length > 0) {
|
|
681
640
|
gitignoreContent += '\n';
|
|
682
641
|
}
|
|
683
|
-
|
|
642
|
+
|
|
684
643
|
// Add the missing entries
|
|
685
644
|
gitignoreContent += missingEntries.join('\n') + '\n';
|
|
686
|
-
|
|
645
|
+
|
|
687
646
|
// Write the updated file
|
|
688
647
|
fs.writeFileSync(gitignorePath, gitignoreContent);
|
|
689
|
-
|
|
648
|
+
|
|
690
649
|
if (fileExists) {
|
|
691
650
|
success('.gitignore updated with Claude entries');
|
|
692
651
|
} else {
|
|
693
652
|
success('.gitignore created with Claude entries');
|
|
694
653
|
}
|
|
695
|
-
|
|
654
|
+
|
|
696
655
|
// Show what was added
|
|
697
656
|
missingEntries.forEach(entry => {
|
|
698
657
|
if (!entry.startsWith('#')) {
|
|
@@ -707,22 +666,22 @@ function updateGitignore() {
|
|
|
707
666
|
// Configure Git (line endings, etc.)
|
|
708
667
|
function configureGit() {
|
|
709
668
|
info('Configuring Git...');
|
|
710
|
-
|
|
669
|
+
|
|
711
670
|
try {
|
|
712
|
-
// Configure line endings
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
try {
|
|
718
|
-
execSync('powershell.exe -Command "git config core.autocrlf true"', { stdio: 'ignore' });
|
|
671
|
+
// Configure line endings based on platform
|
|
672
|
+
// Why: CRLF/LF handling differs between Windows and Unix
|
|
673
|
+
if (isWindows()) {
|
|
674
|
+
// On Windows: Keep CRLF in working directory, convert to LF in repo
|
|
675
|
+
execSync('git config core.autocrlf true', { stdio: 'ignore' });
|
|
719
676
|
success('Line endings configured for Windows (core.autocrlf = true)');
|
|
720
|
-
}
|
|
721
|
-
|
|
677
|
+
} else {
|
|
678
|
+
// On Unix: Keep LF everywhere, convert CRLF to LF on commit
|
|
679
|
+
execSync('git config core.autocrlf input', { stdio: 'ignore' });
|
|
680
|
+
success('Line endings configured for Unix (core.autocrlf = input)');
|
|
722
681
|
}
|
|
723
|
-
|
|
682
|
+
|
|
724
683
|
} catch (e) {
|
|
725
|
-
warning('Error configuring Git');
|
|
684
|
+
warning('Error configuring Git: ' + e.message);
|
|
726
685
|
}
|
|
727
686
|
}
|
|
728
687
|
|
|
@@ -736,7 +695,7 @@ function uninstall() {
|
|
|
736
695
|
|
|
737
696
|
const hooksPath = '.git/hooks';
|
|
738
697
|
const hooks = ['pre-commit', 'prepare-commit-msg'];
|
|
739
|
-
|
|
698
|
+
|
|
740
699
|
hooks.forEach(hook => {
|
|
741
700
|
const hookPath = path.join(hooksPath, hook);
|
|
742
701
|
if (fs.existsSync(hookPath)) {
|
|
@@ -755,11 +714,11 @@ function enable(hookName) {
|
|
|
755
714
|
}
|
|
756
715
|
|
|
757
716
|
const hooks = hookName ? [hookName] : ['pre-commit', 'prepare-commit-msg'];
|
|
758
|
-
|
|
717
|
+
|
|
759
718
|
hooks.forEach(hook => {
|
|
760
719
|
const disabledPath = `.git/hooks/${hook}.disabled`;
|
|
761
720
|
const enabledPath = `.git/hooks/${hook}`;
|
|
762
|
-
|
|
721
|
+
|
|
763
722
|
if (fs.existsSync(disabledPath)) {
|
|
764
723
|
fs.renameSync(disabledPath, enabledPath);
|
|
765
724
|
success(`${hook} enabled`);
|
|
@@ -778,11 +737,11 @@ function disable(hookName) {
|
|
|
778
737
|
}
|
|
779
738
|
|
|
780
739
|
const hooks = hookName ? [hookName] : ['pre-commit', 'prepare-commit-msg'];
|
|
781
|
-
|
|
740
|
+
|
|
782
741
|
hooks.forEach(hook => {
|
|
783
742
|
const enabledPath = `.git/hooks/${hook}`;
|
|
784
743
|
const disabledPath = `.git/hooks/${hook}.disabled`;
|
|
785
|
-
|
|
744
|
+
|
|
786
745
|
if (fs.existsSync(enabledPath)) {
|
|
787
746
|
fs.renameSync(enabledPath, disabledPath);
|
|
788
747
|
success(`${hook} disabled`);
|
|
@@ -795,14 +754,14 @@ function disable(hookName) {
|
|
|
795
754
|
}
|
|
796
755
|
|
|
797
756
|
// Analyze-diff command
|
|
798
|
-
function analyzeDiff(args) {
|
|
757
|
+
async function analyzeDiff(args) {
|
|
799
758
|
if (!checkGitRepo()) {
|
|
800
759
|
error('You are not in a Git repository.');
|
|
801
760
|
return;
|
|
802
761
|
}
|
|
803
762
|
|
|
804
763
|
const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
|
|
805
|
-
|
|
764
|
+
|
|
806
765
|
if (!currentBranch) {
|
|
807
766
|
error('You are not in a valid branch.');
|
|
808
767
|
return;
|
|
@@ -819,7 +778,7 @@ function analyzeDiff(args) {
|
|
|
819
778
|
baseBranch = `origin/${targetBranch}`;
|
|
820
779
|
compareWith = `${baseBranch}...HEAD`;
|
|
821
780
|
contextDescription = `${currentBranch} vs ${baseBranch}`;
|
|
822
|
-
|
|
781
|
+
|
|
823
782
|
// Check that the origin branch exists
|
|
824
783
|
try {
|
|
825
784
|
execSync(`git rev-parse --verify ${baseBranch}`, { stdio: 'ignore' });
|
|
@@ -832,7 +791,7 @@ function analyzeDiff(args) {
|
|
|
832
791
|
baseBranch = `origin/${currentBranch}`;
|
|
833
792
|
compareWith = `${baseBranch}...HEAD`;
|
|
834
793
|
contextDescription = `${currentBranch} vs ${baseBranch}`;
|
|
835
|
-
|
|
794
|
+
|
|
836
795
|
// Check that the origin branch exists
|
|
837
796
|
try {
|
|
838
797
|
execSync(`git rev-parse --verify ${baseBranch}`, { stdio: 'ignore' });
|
|
@@ -841,7 +800,7 @@ function analyzeDiff(args) {
|
|
|
841
800
|
baseBranch = 'origin/develop';
|
|
842
801
|
compareWith = `${baseBranch}...HEAD`;
|
|
843
802
|
contextDescription = `${currentBranch} vs ${baseBranch} (fallback)`;
|
|
844
|
-
|
|
803
|
+
|
|
845
804
|
try {
|
|
846
805
|
execSync(`git rev-parse --verify ${baseBranch}`, { stdio: 'ignore' });
|
|
847
806
|
warning(`Branch origin/${currentBranch} does not exist. Using ${baseBranch} as fallback.`);
|
|
@@ -850,7 +809,7 @@ function analyzeDiff(args) {
|
|
|
850
809
|
baseBranch = 'origin/main';
|
|
851
810
|
compareWith = `${baseBranch}...HEAD`;
|
|
852
811
|
contextDescription = `${currentBranch} vs ${baseBranch} (fallback)`;
|
|
853
|
-
|
|
812
|
+
|
|
854
813
|
try {
|
|
855
814
|
execSync(`git rev-parse --verify ${baseBranch}`, { stdio: 'ignore' });
|
|
856
815
|
warning(`No origin/develop branch. Using ${baseBranch} as fallback.`);
|
|
@@ -868,12 +827,12 @@ function analyzeDiff(args) {
|
|
|
868
827
|
let diffFiles;
|
|
869
828
|
try {
|
|
870
829
|
diffFiles = execSync(`git diff ${compareWith} --name-only`, { encoding: 'utf8' }).trim();
|
|
871
|
-
|
|
830
|
+
|
|
872
831
|
if (!diffFiles) {
|
|
873
832
|
// Check if there are staged or unstaged changes
|
|
874
833
|
const stagedFiles = execSync('git diff --cached --name-only', { encoding: 'utf8' }).trim();
|
|
875
834
|
const unstagedFiles = execSync('git diff --name-only', { encoding: 'utf8' }).trim();
|
|
876
|
-
|
|
835
|
+
|
|
877
836
|
if (stagedFiles || unstagedFiles) {
|
|
878
837
|
warning('No differences with remote, but you have uncommitted local changes.');
|
|
879
838
|
console.log('Staged changes:', stagedFiles || 'none');
|
|
@@ -902,19 +861,26 @@ function analyzeDiff(args) {
|
|
|
902
861
|
error('Error getting diff or commits: ' + e.message);
|
|
903
862
|
return;
|
|
904
863
|
}
|
|
905
|
-
|
|
906
|
-
// Create the prompt for Claude
|
|
907
|
-
const tempDir = `/tmp/claude-analyze-${Date.now()}`;
|
|
908
|
-
fs.mkdirSync(tempDir, { recursive: true });
|
|
909
|
-
|
|
910
|
-
const promptFile = path.join(tempDir, 'prompt.txt');
|
|
911
|
-
const prompt = `Analyze the following changes. CONTEXT: ${contextDescription}
|
|
912
864
|
|
|
865
|
+
// Check if subagents should be used
|
|
866
|
+
const useSubagents = process.env.CLAUDE_USE_SUBAGENTS === 'true';
|
|
867
|
+
const subagentModel = process.env.CLAUDE_SUBAGENT_MODEL || 'haiku';
|
|
868
|
+
let subagentBatchSize = parseInt(process.env.CLAUDE_SUBAGENT_BATCH_SIZE || '3');
|
|
869
|
+
// Validate batch size (must be >= 1)
|
|
870
|
+
if (subagentBatchSize < 1) {
|
|
871
|
+
subagentBatchSize = 1;
|
|
872
|
+
}
|
|
873
|
+
const subagentInstruction = useSubagents
|
|
874
|
+
? `\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`
|
|
875
|
+
: '';
|
|
876
|
+
|
|
877
|
+
const prompt = `Analyze the following changes. CONTEXT: ${contextDescription}
|
|
878
|
+
${subagentInstruction}
|
|
913
879
|
Please generate:
|
|
914
880
|
1. A concise and descriptive PR title (maximum 72 characters)
|
|
915
881
|
2. A detailed PR description that includes:
|
|
916
882
|
- Summary of changes
|
|
917
|
-
- Motivation/context
|
|
883
|
+
- Motivation/context
|
|
918
884
|
- Type of change (feature/fix/refactor/docs/etc)
|
|
919
885
|
- Recommended testing
|
|
920
886
|
3. A suggested branch name following the format: type/short-description (example: feature/add-user-auth, fix/memory-leak)
|
|
@@ -940,67 +906,53 @@ ${diffFiles}
|
|
|
940
906
|
=== FULL DIFF ===
|
|
941
907
|
${fullDiff.substring(0, 50000)} ${fullDiff.length > 50000 ? '\n... (truncated diff)' : ''}`;
|
|
942
908
|
|
|
943
|
-
fs.writeFileSync(promptFile, prompt);
|
|
944
|
-
|
|
945
909
|
info('Sending to Claude for analysis...');
|
|
946
|
-
|
|
910
|
+
const startTime = Date.now();
|
|
911
|
+
|
|
947
912
|
try {
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
console.log('Complete response:', response);
|
|
955
|
-
return;
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
let result;
|
|
959
|
-
try {
|
|
960
|
-
result = JSON.parse(jsonMatch[0]);
|
|
961
|
-
} catch (e) {
|
|
962
|
-
error('Error parsing JSON response: ' + e.message);
|
|
963
|
-
console.log('JSON received:', jsonMatch[0]);
|
|
964
|
-
return;
|
|
965
|
-
}
|
|
966
|
-
|
|
913
|
+
// Use cross-platform executeClaude from claude-client.js
|
|
914
|
+
const response = await executeClaude(prompt, { timeout: 180000 }); // 3 minutes for diff analysis
|
|
915
|
+
|
|
916
|
+
// Extract JSON from response using claude-client utility
|
|
917
|
+
const result = extractJSON(response);
|
|
918
|
+
|
|
967
919
|
// Show the results
|
|
968
920
|
console.log('');
|
|
969
921
|
console.log('════════════════════════════════════════════════════════════════');
|
|
970
922
|
console.log(' DIFFERENCES ANALYSIS ');
|
|
971
923
|
console.log('════════════════════════════════════════════════════════════════');
|
|
972
924
|
console.log('');
|
|
973
|
-
|
|
925
|
+
|
|
974
926
|
console.log(`🔍 ${colors.blue}Context:${colors.reset} ${contextDescription}`);
|
|
975
927
|
console.log(`📊 ${colors.blue}Changed Files:${colors.reset} ${diffFiles.split('\n').length}`);
|
|
976
928
|
console.log('');
|
|
977
|
-
|
|
929
|
+
|
|
978
930
|
console.log(`📝 ${colors.green}Pull Request Title:${colors.reset}`);
|
|
979
931
|
console.log(` ${result.prTitle}`);
|
|
980
932
|
console.log('');
|
|
981
|
-
|
|
933
|
+
|
|
982
934
|
console.log(`🌿 ${colors.green}Suggested branch name:${colors.reset}`);
|
|
983
935
|
console.log(` ${result.suggestedBranchName}`);
|
|
984
936
|
console.log('');
|
|
985
|
-
|
|
937
|
+
|
|
986
938
|
console.log(`📋 ${colors.green}Type of change:${colors.reset} ${result.changeType}`);
|
|
987
|
-
|
|
939
|
+
|
|
988
940
|
if (result.breakingChanges) {
|
|
989
941
|
console.log(`⚠️ ${colors.yellow}Breaking Changes: SÍ${colors.reset}`);
|
|
990
942
|
}
|
|
991
|
-
|
|
943
|
+
|
|
992
944
|
console.log('');
|
|
993
945
|
console.log(`📄 ${colors.green}Pull Request Description:${colors.reset}`);
|
|
994
946
|
console.log('───────────────────────────────────────────────────────────────');
|
|
995
947
|
console.log(result.prDescription);
|
|
996
948
|
console.log('───────────────────────────────────────────────────────────────');
|
|
997
|
-
|
|
949
|
+
|
|
998
950
|
if (result.testingNotes) {
|
|
999
951
|
console.log('');
|
|
1000
952
|
console.log(`🧪 ${colors.green}Testing notes:${colors.reset}`);
|
|
1001
953
|
console.log(result.testingNotes);
|
|
1002
954
|
}
|
|
1003
|
-
|
|
955
|
+
|
|
1004
956
|
// Guardar los resultados en un archivo con contexto
|
|
1005
957
|
const outputData = {
|
|
1006
958
|
...result,
|
|
@@ -1012,12 +964,17 @@ ${fullDiff.substring(0, 50000)} ${fullDiff.length > 50000 ? '\n... (truncated di
|
|
|
1012
964
|
timestamp: new Date().toISOString()
|
|
1013
965
|
}
|
|
1014
966
|
};
|
|
1015
|
-
|
|
967
|
+
|
|
1016
968
|
const outputFile = '.claude-pr-analysis.json';
|
|
1017
969
|
fs.writeFileSync(outputFile, JSON.stringify(outputData, null, 2));
|
|
970
|
+
|
|
971
|
+
const elapsed = Date.now() - startTime;
|
|
972
|
+
const seconds = Math.floor(elapsed / 1000);
|
|
973
|
+
const ms = elapsed % 1000;
|
|
1018
974
|
console.log('');
|
|
975
|
+
console.log(`${colors.blue}⏱️ Analysis completed in ${seconds}.${ms}s${colors.reset}`);
|
|
1019
976
|
info(`Results saved in ${outputFile}`);
|
|
1020
|
-
|
|
977
|
+
|
|
1021
978
|
// Sugerencias contextuales
|
|
1022
979
|
console.log('');
|
|
1023
980
|
if (!args[0] && contextDescription.includes('local changes without push')) {
|
|
@@ -1030,14 +987,11 @@ ${fullDiff.substring(0, 50000)} ${fullDiff.length > 50000 ? '\n... (truncated di
|
|
|
1030
987
|
console.log(`💡 ${colors.yellow}For renaming your current branch:${colors.reset}`);
|
|
1031
988
|
console.log(` git branch -m ${result.suggestedBranchName}`);
|
|
1032
989
|
}
|
|
1033
|
-
|
|
990
|
+
|
|
1034
991
|
console.log(`💡 ${colors.yellow}Tip:${colors.reset} Use this information to create your PR on GitHub.`);
|
|
1035
|
-
|
|
992
|
+
|
|
1036
993
|
} catch (e) {
|
|
1037
994
|
error('Error executing Claude: ' + e.message);
|
|
1038
|
-
} finally {
|
|
1039
|
-
// Clean temporary files
|
|
1040
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
1041
995
|
}
|
|
1042
996
|
}
|
|
1043
997
|
|
|
@@ -1053,7 +1007,7 @@ function status() {
|
|
|
1053
1007
|
hooks.forEach(hook => {
|
|
1054
1008
|
const enabledPath = `.git/hooks/${hook}`;
|
|
1055
1009
|
const disabledPath = `.git/hooks/${hook}.disabled`;
|
|
1056
|
-
|
|
1010
|
+
|
|
1057
1011
|
if (fs.existsSync(enabledPath)) {
|
|
1058
1012
|
success(`${hook}: enabled`);
|
|
1059
1013
|
} else if (fs.existsSync(disabledPath)) {
|
|
@@ -1084,7 +1038,7 @@ function status() {
|
|
|
1084
1038
|
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
|
1085
1039
|
const claudeIgnores = ['.claude/', 'debug-claude-response.json', '.claude-pr-analysis.json'];
|
|
1086
1040
|
let allPresent = true;
|
|
1087
|
-
|
|
1041
|
+
|
|
1088
1042
|
claudeIgnores.forEach(entry => {
|
|
1089
1043
|
const regex = new RegExp(`^${entry.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm');
|
|
1090
1044
|
if (regex.test(gitignoreContent)) {
|
|
@@ -1094,7 +1048,7 @@ function status() {
|
|
|
1094
1048
|
allPresent = false;
|
|
1095
1049
|
}
|
|
1096
1050
|
});
|
|
1097
|
-
|
|
1051
|
+
|
|
1098
1052
|
if (!allPresent) {
|
|
1099
1053
|
info('\nRun "claude-hooks install" to update .gitignore');
|
|
1100
1054
|
}
|
|
@@ -1116,12 +1070,12 @@ function compareVersions(v1, v2) {
|
|
|
1116
1070
|
// Usar el script compartido para mantener consistencia
|
|
1117
1071
|
const result = execSync(`bash -c 'source "${getTemplatesPath()}/check-version.sh" && compare_versions "${v1}" "${v2}"; echo $?'`, { encoding: 'utf8' }).trim();
|
|
1118
1072
|
const exitCode = parseInt(result);
|
|
1119
|
-
|
|
1073
|
+
|
|
1120
1074
|
// Convertir los códigos de retorno del script bash a valores JS
|
|
1121
1075
|
if (exitCode === 0) return 0; // iguales
|
|
1122
1076
|
if (exitCode === 1) return 1; // v1 > v2
|
|
1123
1077
|
if (exitCode === 2) return -1; // v1 < v2
|
|
1124
|
-
|
|
1078
|
+
|
|
1125
1079
|
// Fallback: comparación simple si el script falla
|
|
1126
1080
|
if (v1 === v2) return 0;
|
|
1127
1081
|
const sorted = [v1, v2].sort((a, b) => {
|
|
@@ -1145,13 +1099,13 @@ function compareVersions(v1, v2) {
|
|
|
1145
1099
|
// Update command - update to the latest version
|
|
1146
1100
|
async function update() {
|
|
1147
1101
|
info('Checking latest available version...');
|
|
1148
|
-
|
|
1102
|
+
|
|
1149
1103
|
try {
|
|
1150
|
-
const currentVersion =
|
|
1104
|
+
const currentVersion = getPackageJson().version;
|
|
1151
1105
|
const latestVersion = await getLatestVersion('claude-git-hooks');
|
|
1152
|
-
|
|
1106
|
+
|
|
1153
1107
|
const comparison = compareVersions(currentVersion, latestVersion);
|
|
1154
|
-
|
|
1108
|
+
|
|
1155
1109
|
if (comparison === 0) {
|
|
1156
1110
|
success(`You already have the latest version installed (${currentVersion})`);
|
|
1157
1111
|
return;
|
|
@@ -1161,20 +1115,20 @@ async function update() {
|
|
|
1161
1115
|
success(`You already have the latest version installed (${currentVersion})`);
|
|
1162
1116
|
return;
|
|
1163
1117
|
}
|
|
1164
|
-
|
|
1118
|
+
|
|
1165
1119
|
info(`Current version: ${currentVersion}`);
|
|
1166
1120
|
info(`Available version: ${latestVersion}`);
|
|
1167
|
-
|
|
1121
|
+
|
|
1168
1122
|
// Actualizar el paquete
|
|
1169
1123
|
info('Updating claude-git-hooks...');
|
|
1170
1124
|
try {
|
|
1171
1125
|
execSync('npm install -g claude-git-hooks@latest', { stdio: 'inherit' });
|
|
1172
1126
|
success(`Successfully updated to version ${latestVersion}`);
|
|
1173
|
-
|
|
1127
|
+
|
|
1174
1128
|
// Reinstall hooks with the new version
|
|
1175
1129
|
info('Reinstalling hooks with the new version...');
|
|
1176
1130
|
await install(['--force']);
|
|
1177
|
-
|
|
1131
|
+
|
|
1178
1132
|
} catch (updateError) {
|
|
1179
1133
|
error('Error updating. Try running: npm install -g claude-git-hooks@latest');
|
|
1180
1134
|
}
|
|
@@ -1241,6 +1195,18 @@ Exclude code from analysis:
|
|
|
1241
1195
|
...excluded code...
|
|
1242
1196
|
// SKIP_ANALYSIS_BLOCK
|
|
1243
1197
|
|
|
1198
|
+
Performance optimization (NEW in v1.5.5):
|
|
1199
|
+
export CLAUDE_USE_SUBAGENTS=true # Enable parallel analysis for 3+ files
|
|
1200
|
+
export CLAUDE_SUBAGENT_MODEL=haiku # Model: haiku (fast), sonnet, opus
|
|
1201
|
+
export CLAUDE_SUBAGENT_BATCH_SIZE=3 # Parallel subagents per batch (default: 3)
|
|
1202
|
+
|
|
1203
|
+
# Batching examples:
|
|
1204
|
+
# BATCH_SIZE=1 with 4 files → 4 sequential batches (1 subagent each)
|
|
1205
|
+
# BATCH_SIZE=3 with 4 files → 2 batches (3 parallel, then 1)
|
|
1206
|
+
# BATCH_SIZE=4 with 4 files → 1 batch (4 parallel subagents)
|
|
1207
|
+
|
|
1208
|
+
# Benefits: Faster for multi-file commits, shows execution time
|
|
1209
|
+
|
|
1244
1210
|
More information: https://github.com/pablorovito/claude-git-hooks
|
|
1245
1211
|
`);
|
|
1246
1212
|
}
|
|
@@ -1270,7 +1236,7 @@ async function main() {
|
|
|
1270
1236
|
status();
|
|
1271
1237
|
break;
|
|
1272
1238
|
case 'analyze-diff':
|
|
1273
|
-
analyzeDiff(args.slice(1));
|
|
1239
|
+
await analyzeDiff(args.slice(1));
|
|
1274
1240
|
break;
|
|
1275
1241
|
case 'help':
|
|
1276
1242
|
case '--help':
|