claude-git-hooks 1.5.1 → 1.5.2
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 +49 -3
- package/README.md +1 -1
- package/bin/claude-hooks +334 -334
- package/package.json +1 -1
- package/templates/pre-commit +91 -91
- package/templates/prepare-commit-msg +32 -32
package/bin/claude-hooks
CHANGED
|
@@ -7,24 +7,24 @@ const os = require('os');
|
|
|
7
7
|
const readline = require('readline');
|
|
8
8
|
const https = require('https');
|
|
9
9
|
|
|
10
|
-
//
|
|
10
|
+
// Function to get the latest version from NPM
|
|
11
11
|
function getLatestVersion(packageName) {
|
|
12
12
|
return new Promise((resolve, reject) => {
|
|
13
|
-
//
|
|
13
|
+
// Use the main NPM API, not /latest
|
|
14
14
|
https.get(`https://registry.npmjs.org/${packageName}`, (res) => {
|
|
15
15
|
let data = '';
|
|
16
16
|
res.on('data', chunk => data += chunk);
|
|
17
17
|
res.on('end', () => {
|
|
18
18
|
try {
|
|
19
19
|
const json = JSON.parse(data);
|
|
20
|
-
//
|
|
20
|
+
// Get the version from the 'latest' tag
|
|
21
21
|
if (json['dist-tags'] && json['dist-tags'].latest) {
|
|
22
22
|
resolve(json['dist-tags'].latest);
|
|
23
23
|
} else {
|
|
24
|
-
reject(new Error('
|
|
24
|
+
reject(new Error('Could not get the version'));
|
|
25
25
|
}
|
|
26
26
|
} catch (e) {
|
|
27
|
-
//
|
|
27
|
+
// If it fails, try with npm view
|
|
28
28
|
try {
|
|
29
29
|
const version = execSync(`npm view ${packageName} version`, { encoding: 'utf8' }).trim();
|
|
30
30
|
resolve(version);
|
|
@@ -37,57 +37,57 @@ function getLatestVersion(packageName) {
|
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
//
|
|
40
|
+
// Function to check version (used by hooks)
|
|
41
41
|
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
|
-
return true; //
|
|
47
|
+
return true; // Already updated
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
console.log('');
|
|
51
|
-
warning(`
|
|
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
|
-
rl.question('
|
|
60
|
+
rl.question('Do you want to update now? (y/n): ', (answer) => {
|
|
61
61
|
rl.close();
|
|
62
62
|
|
|
63
|
-
if (answer.toLowerCase() === '
|
|
64
|
-
info('
|
|
63
|
+
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
|
64
|
+
info('Updating claude-git-hooks...');
|
|
65
65
|
try {
|
|
66
66
|
execSync('npm install -g claude-git-hooks@latest', { stdio: 'inherit' });
|
|
67
|
-
success('
|
|
68
|
-
process.exit(0); //
|
|
67
|
+
success('Update completed. Please run your command again.');
|
|
68
|
+
process.exit(0); // Exit so user restarts the process
|
|
69
69
|
} catch (e) {
|
|
70
|
-
error('Error
|
|
70
|
+
error('Error updating: ' + e.message);
|
|
71
71
|
resolve(false);
|
|
72
72
|
}
|
|
73
73
|
} else {
|
|
74
|
-
info('
|
|
75
|
-
resolve(true); //
|
|
74
|
+
info('Update postponed. You can update later with: claude-hooks update');
|
|
75
|
+
resolve(true); // Continue without updating
|
|
76
76
|
}
|
|
77
77
|
});
|
|
78
78
|
});
|
|
79
79
|
} catch (e) {
|
|
80
|
-
//
|
|
80
|
+
// If there's an error checking version, continue without blocking
|
|
81
81
|
return true;
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
//
|
|
85
|
+
// Export for use in hooks
|
|
86
86
|
if (typeof module !== 'undefined' && module.exports) {
|
|
87
87
|
module.exports = { checkVersionAndPromptUpdate };
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
//
|
|
90
|
+
// Colors for output
|
|
91
91
|
const colors = {
|
|
92
92
|
reset: '\x1b[0m',
|
|
93
93
|
red: '\x1b[31m',
|
|
@@ -117,7 +117,7 @@ function warning(message) {
|
|
|
117
117
|
log(`⚠️ ${message}`, 'yellow');
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
//
|
|
120
|
+
// Function to read password securely
|
|
121
121
|
function readPassword(prompt) {
|
|
122
122
|
return new Promise((resolve) => {
|
|
123
123
|
const rl = readline.createInterface({
|
|
@@ -125,7 +125,7 @@ function readPassword(prompt) {
|
|
|
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) {
|
|
131
131
|
if (rl.stdoutMuted)
|
|
@@ -136,13 +136,13 @@ function readPassword(prompt) {
|
|
|
136
136
|
|
|
137
137
|
rl.question(prompt, (password) => {
|
|
138
138
|
rl.close();
|
|
139
|
-
console.log(); //
|
|
139
|
+
console.log(); // New line
|
|
140
140
|
resolve(password);
|
|
141
141
|
});
|
|
142
142
|
});
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
//
|
|
145
|
+
// Check if sudo password is correct
|
|
146
146
|
function testSudoPassword(password) {
|
|
147
147
|
try {
|
|
148
148
|
execSync('echo "' + password + '" | sudo -S true', {
|
|
@@ -155,7 +155,7 @@ function testSudoPassword(password) {
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
//
|
|
158
|
+
// Install package with automatic sudo
|
|
159
159
|
function installPackage(packageName, sudoPassword = null) {
|
|
160
160
|
try {
|
|
161
161
|
if (sudoPassword) {
|
|
@@ -181,22 +181,22 @@ function installPackage(packageName, sudoPassword = null) {
|
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
//
|
|
184
|
+
// Entertainment system
|
|
185
185
|
class Entertainment {
|
|
186
186
|
static jokes = [
|
|
187
|
-
"
|
|
188
|
-
"
|
|
189
|
-
"
|
|
190
|
-
"
|
|
191
|
-
"
|
|
192
|
-
"
|
|
193
|
-
"
|
|
194
|
-
"
|
|
187
|
+
"Why do programmers prefer dark mode? Because light attracts bugs!",
|
|
188
|
+
"A QA engineer walks into a bar. Orders 1 beer. Orders 0 beers. Orders -1 beers.",
|
|
189
|
+
"What's a pirate's favorite programming language? R!",
|
|
190
|
+
"There are 10 types of people: those who understand binary and those who don't.",
|
|
191
|
+
"Why do programmers confuse Halloween with Christmas? Because Oct 31 = Dec 25",
|
|
192
|
+
"What does one bit say to another? See you on the bus!",
|
|
193
|
+
"Why don't Java and C++ get along? Because they have different views on pointers.",
|
|
194
|
+
"My code doesn't have bugs, just undocumented features."
|
|
195
195
|
];
|
|
196
196
|
|
|
197
197
|
static async getJoke() {
|
|
198
198
|
return new Promise((resolve) => {
|
|
199
|
-
//
|
|
199
|
+
// Try to get joke from API
|
|
200
200
|
const req = https.get('https://icanhazdadjoke.com/', {
|
|
201
201
|
headers: { 'Accept': 'text/plain' },
|
|
202
202
|
timeout: 3000
|
|
@@ -207,7 +207,7 @@ class Entertainment {
|
|
|
207
207
|
});
|
|
208
208
|
|
|
209
209
|
req.on('error', () => {
|
|
210
|
-
//
|
|
210
|
+
// If it fails, use local joke
|
|
211
211
|
const randomJoke = this.jokes[Math.floor(Math.random() * this.jokes.length)];
|
|
212
212
|
resolve(randomJoke);
|
|
213
213
|
});
|
|
@@ -220,7 +220,7 @@ class Entertainment {
|
|
|
220
220
|
});
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
static async showSpinner(promise, message = '
|
|
223
|
+
static async showSpinner(promise, message = 'Processing') {
|
|
224
224
|
const spinners = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
225
225
|
let spinnerIndex = 0;
|
|
226
226
|
let jokeCountdown = 10;
|
|
@@ -228,15 +228,15 @@ class Entertainment {
|
|
|
228
228
|
let isFinished = false;
|
|
229
229
|
let isFirstRender = true;
|
|
230
230
|
|
|
231
|
-
//
|
|
231
|
+
// Get first joke from API without blocking
|
|
232
232
|
this.getJoke().then(joke => {
|
|
233
233
|
if (!isFinished) currentJoke = joke;
|
|
234
|
-
}).catch(() => {}); //
|
|
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(() => {
|
|
@@ -247,11 +247,11 @@ class Entertainment {
|
|
|
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 => {
|
|
257
257
|
if (!isFinished) currentJoke = joke;
|
|
@@ -264,20 +264,20 @@ class Entertainment {
|
|
|
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
|
-
//
|
|
280
|
-
process.stdout.write('\r\x1B[2K' + `${colors.yellow}⏱️
|
|
279
|
+
// Line 3: Countdown
|
|
280
|
+
process.stdout.write('\r\x1B[2K' + `${colors.yellow}⏱️ Next joke in: ${jokeCountdown}s${colors.reset}\n`);
|
|
281
281
|
}, 100);
|
|
282
282
|
|
|
283
283
|
try {
|
|
@@ -285,15 +285,15 @@ class Entertainment {
|
|
|
285
285
|
isFinished = true;
|
|
286
286
|
clearInterval(interval);
|
|
287
287
|
|
|
288
|
-
//
|
|
289
|
-
process.stdout.write('\x1B[3A'); //
|
|
290
|
-
process.stdout.write('\r\x1B[2K'); //
|
|
291
|
-
process.stdout.write('\n\r\x1B[2K'); //
|
|
292
|
-
process.stdout.write('\n\r\x1B[2K'); //
|
|
293
|
-
process.stdout.write('\x1B[2A'); //
|
|
294
|
-
process.stdout.write('\r'); //
|
|
288
|
+
// Clean exactly 3 lines completely
|
|
289
|
+
process.stdout.write('\x1B[3A'); // Go up 3 lines
|
|
290
|
+
process.stdout.write('\r\x1B[2K'); // Clean line 1
|
|
291
|
+
process.stdout.write('\n\r\x1B[2K'); // Go down and clean line 2
|
|
292
|
+
process.stdout.write('\n\r\x1B[2K'); // Go down and clean line 3
|
|
293
|
+
process.stdout.write('\x1B[2A'); // Go up 2 lines to end up on the first
|
|
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;
|
|
@@ -301,15 +301,15 @@ class Entertainment {
|
|
|
301
301
|
isFinished = true;
|
|
302
302
|
clearInterval(interval);
|
|
303
303
|
|
|
304
|
-
//
|
|
305
|
-
process.stdout.write('\x1B[3A'); //
|
|
306
|
-
process.stdout.write('\r\x1B[2K'); //
|
|
307
|
-
process.stdout.write('\n\r\x1B[2K'); //
|
|
308
|
-
process.stdout.write('\n\r\x1B[2K'); //
|
|
309
|
-
process.stdout.write('\x1B[2A'); //
|
|
310
|
-
process.stdout.write('\r'); //
|
|
304
|
+
// Clean exactly 3 lines completely
|
|
305
|
+
process.stdout.write('\x1B[3A'); // Go up 3 lines
|
|
306
|
+
process.stdout.write('\r\x1B[2K'); // Clean line 1
|
|
307
|
+
process.stdout.write('\n\r\x1B[2K'); // Go down and clean line 2
|
|
308
|
+
process.stdout.write('\n\r\x1B[2K'); // Go down and clean line 3
|
|
309
|
+
process.stdout.write('\x1B[2A'); // Go up 2 lines to end up on the first
|
|
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;
|
|
@@ -317,7 +317,7 @@ class Entertainment {
|
|
|
317
317
|
}
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
-
//
|
|
320
|
+
// Check if we are in a git repository
|
|
321
321
|
function checkGitRepo() {
|
|
322
322
|
try {
|
|
323
323
|
execSync('git rev-parse --git-dir', { stdio: 'ignore' });
|
|
@@ -327,92 +327,92 @@ function checkGitRepo() {
|
|
|
327
327
|
}
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
-
//
|
|
330
|
+
// Get the templates path
|
|
331
331
|
function getTemplatesPath() {
|
|
332
332
|
return path.join(__dirname, '..', 'templates');
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
-
//
|
|
335
|
+
// Install command
|
|
336
336
|
async function install(args) {
|
|
337
337
|
if (!checkGitRepo()) {
|
|
338
|
-
error('
|
|
338
|
+
error('You are not in a Git repository. Please run this command from the root of a repository.');
|
|
339
339
|
}
|
|
340
340
|
|
|
341
341
|
const isForce = args.includes('--force');
|
|
342
342
|
const skipAuth = args.includes('--skip-auth');
|
|
343
343
|
|
|
344
344
|
if (isForce) {
|
|
345
|
-
info('
|
|
345
|
+
info('Installing Claude Git Hooks (force mode)...');
|
|
346
346
|
} else {
|
|
347
|
-
info('
|
|
347
|
+
info('Installing Claude Git Hooks...');
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
-
//
|
|
350
|
+
// Request sudo password at the beginning if necessary
|
|
351
351
|
let sudoPassword = null;
|
|
352
352
|
if (os.platform() === 'linux') {
|
|
353
353
|
const needsInstall = await checkIfInstallationNeeded();
|
|
354
354
|
if (needsInstall) {
|
|
355
|
-
info('
|
|
356
|
-
sudoPassword = await readPassword('
|
|
355
|
+
info('Sudo access is needed for automatic dependency installation, please enter password');
|
|
356
|
+
sudoPassword = await readPassword('Enter your Ubuntu password for sudo: ');
|
|
357
357
|
|
|
358
358
|
if (sudoPassword && !testSudoPassword(sudoPassword)) {
|
|
359
|
-
warning('
|
|
359
|
+
warning('Incorrect password. Continuing without automatic installation.');
|
|
360
360
|
sudoPassword = null;
|
|
361
361
|
} else if (sudoPassword) {
|
|
362
|
-
success('
|
|
362
|
+
success('Password verified. Proceeding with automatic installation.');
|
|
363
363
|
}
|
|
364
364
|
}
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
-
//
|
|
367
|
+
// Check dependencies with automatic installation
|
|
368
368
|
await checkAndInstallDependencies(sudoPassword, skipAuth);
|
|
369
369
|
|
|
370
370
|
const templatesPath = getTemplatesPath();
|
|
371
371
|
const hooksPath = '.git/hooks';
|
|
372
372
|
|
|
373
|
-
//
|
|
373
|
+
// Create hooks directory if it doesn't exist
|
|
374
374
|
if (!fs.existsSync(hooksPath)) {
|
|
375
375
|
fs.mkdirSync(hooksPath, { recursive: true });
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
-
// Hooks
|
|
378
|
+
// Hooks to install
|
|
379
379
|
const hooks = ['pre-commit', 'prepare-commit-msg'];
|
|
380
380
|
|
|
381
381
|
hooks.forEach(hook => {
|
|
382
382
|
const sourcePath = path.join(templatesPath, hook);
|
|
383
383
|
const destPath = path.join(hooksPath, hook);
|
|
384
384
|
|
|
385
|
-
//
|
|
385
|
+
// Make backup if it exists
|
|
386
386
|
if (fs.existsSync(destPath)) {
|
|
387
387
|
const backupPath = `${destPath}.backup.${Date.now()}`;
|
|
388
388
|
fs.copyFileSync(destPath, backupPath);
|
|
389
|
-
info(`Backup
|
|
389
|
+
info(`Backup created: ${backupPath}`);
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
-
//
|
|
392
|
+
// Copy hook
|
|
393
393
|
fs.copyFileSync(sourcePath, destPath);
|
|
394
394
|
fs.chmodSync(destPath, '755');
|
|
395
|
-
success(`${hook}
|
|
395
|
+
success(`${hook} installed`);
|
|
396
396
|
});
|
|
397
397
|
|
|
398
|
-
//
|
|
398
|
+
// Copy version verification script
|
|
399
399
|
const checkVersionSource = path.join(templatesPath, 'check-version.sh');
|
|
400
400
|
const checkVersionDest = path.join(hooksPath, 'check-version.sh');
|
|
401
401
|
|
|
402
402
|
if (fs.existsSync(checkVersionSource)) {
|
|
403
403
|
fs.copyFileSync(checkVersionSource, checkVersionDest);
|
|
404
404
|
fs.chmodSync(checkVersionDest, '755');
|
|
405
|
-
success('
|
|
405
|
+
success('Version verification script installed');
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
-
//
|
|
408
|
+
// Create .claude directory if it doesn't exist
|
|
409
409
|
const claudeDir = '.claude';
|
|
410
410
|
if (!fs.existsSync(claudeDir)) {
|
|
411
411
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
412
412
|
success('.claude directory created');
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
-
//
|
|
415
|
+
// Copy guidelines and prompts files to .claude
|
|
416
416
|
const claudeFiles = [
|
|
417
417
|
'CLAUDE_PRE_COMMIT_SONAR.md',
|
|
418
418
|
'CLAUDE_ANALYSIS_PROMPT_SONAR.md',
|
|
@@ -423,101 +423,101 @@ async function install(args) {
|
|
|
423
423
|
const destPath = path.join(claudeDir, file);
|
|
424
424
|
const sourcePath = path.join(templatesPath, file);
|
|
425
425
|
|
|
426
|
-
//
|
|
426
|
+
// In force mode or if it doesn't exist, copy the file
|
|
427
427
|
if (isForce || !fs.existsSync(destPath)) {
|
|
428
428
|
if (fs.existsSync(sourcePath)) {
|
|
429
429
|
fs.copyFileSync(sourcePath, destPath);
|
|
430
|
-
success(`${file}
|
|
430
|
+
success(`${file} installed in .claude/`);
|
|
431
431
|
} else {
|
|
432
|
-
warning(`
|
|
432
|
+
warning(`Template file not found: ${file}`);
|
|
433
433
|
}
|
|
434
434
|
}
|
|
435
435
|
});
|
|
436
436
|
|
|
437
|
-
//
|
|
437
|
+
// Configure Git
|
|
438
438
|
configureGit();
|
|
439
439
|
|
|
440
|
-
//
|
|
440
|
+
// Update .gitignore
|
|
441
441
|
updateGitignore();
|
|
442
442
|
|
|
443
|
-
success('
|
|
444
|
-
console.log('\
|
|
445
|
-
console.log(' git commit -m "auto" #
|
|
446
|
-
console.log(' git commit -m "
|
|
447
|
-
console.log(' git commit --no-verify #
|
|
448
|
-
console.log('\
|
|
449
|
-
console.log(' // SKIP-ANALYSIS #
|
|
450
|
-
console.log(' // SKIP_ANALYSIS_BLOCK #
|
|
451
|
-
console.log(' ...
|
|
443
|
+
success('Claude Git Hooks installed successfully! 🎉');
|
|
444
|
+
console.log('\nUsage:');
|
|
445
|
+
console.log(' git commit -m "auto" # Generate message automatically');
|
|
446
|
+
console.log(' git commit -m "message" # Analyze code before commit');
|
|
447
|
+
console.log(' git commit --no-verify # Skip analysis completely');
|
|
448
|
+
console.log('\nExclude code from analysis:');
|
|
449
|
+
console.log(' // SKIP-ANALYSIS # Exclude the next line');
|
|
450
|
+
console.log(' // SKIP_ANALYSIS_BLOCK # Exclude block until finding another equal one');
|
|
451
|
+
console.log(' ...excluded code...');
|
|
452
452
|
console.log(' // SKIP_ANALYSIS_BLOCK');
|
|
453
|
-
console.log('\
|
|
453
|
+
console.log('\nFor more options: claude-hooks --help');
|
|
454
454
|
}
|
|
455
455
|
|
|
456
|
-
//
|
|
456
|
+
// Check complete dependencies (like setup-wsl.sh)
|
|
457
457
|
async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false) {
|
|
458
|
-
info('
|
|
458
|
+
info('Checking system dependencies...');
|
|
459
459
|
|
|
460
|
-
//
|
|
460
|
+
// Check Node.js
|
|
461
461
|
try {
|
|
462
462
|
const nodeVersion = execSync('node --version', { encoding: 'utf8' }).trim();
|
|
463
463
|
success(`Node.js ${nodeVersion}`);
|
|
464
464
|
} catch (e) {
|
|
465
|
-
error('Node.js
|
|
465
|
+
error('Node.js is not installed. Install Node.js and try again.');
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
-
//
|
|
468
|
+
// Check npm
|
|
469
469
|
try {
|
|
470
470
|
const npmVersion = execSync('npm --version', { encoding: 'utf8' }).trim();
|
|
471
471
|
success(`npm ${npmVersion}`);
|
|
472
472
|
} catch (e) {
|
|
473
|
-
error('npm
|
|
473
|
+
error('npm is not installed.');
|
|
474
474
|
}
|
|
475
475
|
|
|
476
|
-
//
|
|
476
|
+
// Check and install jq
|
|
477
477
|
try {
|
|
478
478
|
const jqVersion = execSync('jq --version', { encoding: 'utf8' }).trim();
|
|
479
479
|
success(`jq ${jqVersion}`);
|
|
480
480
|
} catch (e) {
|
|
481
|
-
warning('jq
|
|
481
|
+
warning('jq is not installed. Installing...');
|
|
482
482
|
if (installPackage('jq', sudoPassword)) {
|
|
483
|
-
success('jq
|
|
483
|
+
success('jq installed successfully');
|
|
484
484
|
} else {
|
|
485
|
-
warning('
|
|
485
|
+
warning('Could not install jq automatically');
|
|
486
486
|
if (os.platform() === 'linux') {
|
|
487
|
-
console.log('
|
|
487
|
+
console.log('Install it manually with: sudo apt install jq');
|
|
488
488
|
} else if (os.platform() === 'darwin') {
|
|
489
|
-
console.log('
|
|
489
|
+
console.log('Install it manually with: brew install jq');
|
|
490
490
|
}
|
|
491
491
|
}
|
|
492
492
|
}
|
|
493
493
|
|
|
494
|
-
//
|
|
494
|
+
// Check and install curl
|
|
495
495
|
try {
|
|
496
496
|
const curlVersion = execSync('curl --version', { encoding: 'utf8' }).split('\n')[0];
|
|
497
497
|
success(`curl ${curlVersion.split(' ')[1]}`);
|
|
498
498
|
} catch (e) {
|
|
499
|
-
warning('curl
|
|
499
|
+
warning('curl is not installed. Installing...');
|
|
500
500
|
if (installPackage('curl', sudoPassword)) {
|
|
501
|
-
success('curl
|
|
501
|
+
success('curl installed successfully');
|
|
502
502
|
} else {
|
|
503
|
-
warning('
|
|
503
|
+
warning('Could not install curl automatically');
|
|
504
504
|
if (os.platform() === 'linux') {
|
|
505
|
-
console.log('
|
|
505
|
+
console.log('Install it manually with: sudo apt install curl');
|
|
506
506
|
} else if (os.platform() === 'darwin') {
|
|
507
|
-
console.log('
|
|
507
|
+
console.log('Install it manually with: brew install curl');
|
|
508
508
|
}
|
|
509
509
|
}
|
|
510
510
|
}
|
|
511
511
|
|
|
512
|
-
//
|
|
512
|
+
// Check Git
|
|
513
513
|
try {
|
|
514
514
|
const gitVersion = execSync('git --version', { encoding: 'utf8' }).trim();
|
|
515
515
|
success(`${gitVersion}`);
|
|
516
516
|
} catch (e) {
|
|
517
|
-
error('Git
|
|
517
|
+
error('Git is not installed. Install Git and try again.');
|
|
518
518
|
}
|
|
519
519
|
|
|
520
|
-
//
|
|
520
|
+
// Check standard Unix tools
|
|
521
521
|
const unixTools = ['sed', 'awk', 'grep', 'head', 'tail', 'stat', 'tput'];
|
|
522
522
|
const missingTools = [];
|
|
523
523
|
|
|
@@ -530,26 +530,26 @@ async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false
|
|
|
530
530
|
});
|
|
531
531
|
|
|
532
532
|
if (missingTools.length === 0) {
|
|
533
|
-
success('
|
|
533
|
+
success('Standard Unix tools verified');
|
|
534
534
|
} else {
|
|
535
|
-
error(`
|
|
535
|
+
error(`Missing standard Unix tools: ${missingTools.join(', ')}. Retry installation in an Ubuntu console`);
|
|
536
536
|
}
|
|
537
537
|
|
|
538
|
-
//
|
|
538
|
+
// Check and install Claude CLI
|
|
539
539
|
await checkAndInstallClaude();
|
|
540
540
|
|
|
541
|
-
//
|
|
541
|
+
// Check Claude authentication (if not skipped)
|
|
542
542
|
if (!skipAuth) {
|
|
543
543
|
await checkClaudeAuth();
|
|
544
544
|
} else {
|
|
545
|
-
warning('
|
|
545
|
+
warning('Skipping Claude authentication verification (--skip-auth)');
|
|
546
546
|
}
|
|
547
547
|
|
|
548
|
-
//
|
|
548
|
+
// Clear password from memory
|
|
549
549
|
sudoPassword = null;
|
|
550
550
|
}
|
|
551
551
|
|
|
552
|
-
//
|
|
552
|
+
// Check if we need to install dependencies
|
|
553
553
|
async function checkIfInstallationNeeded() {
|
|
554
554
|
const dependencies = ['jq', 'curl'];
|
|
555
555
|
|
|
@@ -557,7 +557,7 @@ async function checkIfInstallationNeeded() {
|
|
|
557
557
|
try {
|
|
558
558
|
execSync(`which ${dep}`, { stdio: 'ignore' });
|
|
559
559
|
} catch (e) {
|
|
560
|
-
return true; //
|
|
560
|
+
return true; // Needs installation
|
|
561
561
|
}
|
|
562
562
|
}
|
|
563
563
|
|
|
@@ -565,33 +565,33 @@ async function checkIfInstallationNeeded() {
|
|
|
565
565
|
try {
|
|
566
566
|
execSync('claude --version', { stdio: 'ignore' });
|
|
567
567
|
} catch (e) {
|
|
568
|
-
return true; //
|
|
568
|
+
return true; // Needs Claude installation
|
|
569
569
|
}
|
|
570
570
|
|
|
571
571
|
return false;
|
|
572
572
|
}
|
|
573
573
|
|
|
574
|
-
//
|
|
574
|
+
// Check and install Claude CLI
|
|
575
575
|
async function checkAndInstallClaude() {
|
|
576
576
|
try {
|
|
577
577
|
execSync('claude --version', { stdio: 'ignore' });
|
|
578
|
-
success('Claude CLI
|
|
578
|
+
success('Claude CLI detected');
|
|
579
579
|
} catch (e) {
|
|
580
|
-
info('Claude CLI
|
|
580
|
+
info('Claude CLI not detected. Installing...');
|
|
581
581
|
try {
|
|
582
582
|
execSync('npm install -g @anthropic-ai/claude-cli', { stdio: 'inherit' });
|
|
583
|
-
success('Claude CLI
|
|
583
|
+
success('Claude CLI installed successfully');
|
|
584
584
|
} catch (installError) {
|
|
585
|
-
error('Error
|
|
585
|
+
error('Error installing Claude CLI. Install manually: npm install -g @anthropic-ai/claude-cli');
|
|
586
586
|
}
|
|
587
587
|
}
|
|
588
588
|
}
|
|
589
589
|
|
|
590
|
-
//
|
|
590
|
+
// Check Claude authentication with entertainment
|
|
591
591
|
async function checkClaudeAuth() {
|
|
592
|
-
info('
|
|
592
|
+
info('Checking Claude authentication...');
|
|
593
593
|
|
|
594
|
-
//
|
|
594
|
+
// Use spawn to not block, but with stdio: 'ignore' like the original
|
|
595
595
|
const authPromise = new Promise((resolve, reject) => {
|
|
596
596
|
const child = spawn('claude', ['auth', 'status'], {
|
|
597
597
|
stdio: 'ignore', // Igual que el original
|
|
@@ -599,7 +599,7 @@ async function checkClaudeAuth() {
|
|
|
599
599
|
windowsHide: true
|
|
600
600
|
});
|
|
601
601
|
|
|
602
|
-
//
|
|
602
|
+
// Manual timeout since spawn doesn't have native timeout
|
|
603
603
|
const timeout = setTimeout(() => {
|
|
604
604
|
child.kill();
|
|
605
605
|
reject(new Error('timeout'));
|
|
@@ -621,18 +621,18 @@ async function checkClaudeAuth() {
|
|
|
621
621
|
});
|
|
622
622
|
|
|
623
623
|
try {
|
|
624
|
-
await Entertainment.showSpinner(authPromise, '
|
|
625
|
-
success('
|
|
624
|
+
await Entertainment.showSpinner(authPromise, 'Checking Claude authentication');
|
|
625
|
+
success('Authenticated in Claude');
|
|
626
626
|
} catch (e) {
|
|
627
|
-
warning('
|
|
628
|
-
console.log('
|
|
629
|
-
console.log('
|
|
627
|
+
warning('You are not authenticated in Claude');
|
|
628
|
+
console.log('Run: claude auth login');
|
|
629
|
+
console.log('Then run this command again');
|
|
630
630
|
}
|
|
631
631
|
}
|
|
632
632
|
|
|
633
|
-
//
|
|
633
|
+
// Update .gitignore with Claude entries
|
|
634
634
|
function updateGitignore() {
|
|
635
|
-
info('
|
|
635
|
+
info('Updating .gitignore...');
|
|
636
636
|
|
|
637
637
|
const gitignorePath = '.gitignore';
|
|
638
638
|
const claudeEntries = [
|
|
@@ -646,22 +646,22 @@ function updateGitignore() {
|
|
|
646
646
|
let gitignoreContent = '';
|
|
647
647
|
let fileExists = false;
|
|
648
648
|
|
|
649
|
-
//
|
|
649
|
+
// Read existing .gitignore if it exists
|
|
650
650
|
if (fs.existsSync(gitignorePath)) {
|
|
651
651
|
gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
|
652
652
|
fileExists = true;
|
|
653
653
|
}
|
|
654
654
|
|
|
655
|
-
//
|
|
655
|
+
// Check which entries are missing
|
|
656
656
|
const missingEntries = [];
|
|
657
657
|
claudeEntries.forEach(entry => {
|
|
658
658
|
if (entry.startsWith('#')) {
|
|
659
|
-
//
|
|
659
|
+
// For comments, check if any Claude comment already exists
|
|
660
660
|
if (!gitignoreContent.includes('# Claude')) {
|
|
661
661
|
missingEntries.push(entry);
|
|
662
662
|
}
|
|
663
663
|
} else {
|
|
664
|
-
//
|
|
664
|
+
// For normal entries, check if they are already present
|
|
665
665
|
const regex = new RegExp(`^${entry.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm');
|
|
666
666
|
if (!regex.test(gitignoreContent)) {
|
|
667
667
|
missingEntries.push(entry);
|
|
@@ -669,70 +669,70 @@ function updateGitignore() {
|
|
|
669
669
|
}
|
|
670
670
|
});
|
|
671
671
|
|
|
672
|
-
//
|
|
672
|
+
// If there are missing entries, add them
|
|
673
673
|
if (missingEntries.length > 0) {
|
|
674
|
-
//
|
|
674
|
+
// Ensure there's a newline at the end if the file exists and is not empty
|
|
675
675
|
if (fileExists && gitignoreContent.length > 0 && !gitignoreContent.endsWith('\n')) {
|
|
676
676
|
gitignoreContent += '\n';
|
|
677
677
|
}
|
|
678
678
|
|
|
679
|
-
//
|
|
679
|
+
// If the file is not empty, add a blank line before
|
|
680
680
|
if (gitignoreContent.length > 0) {
|
|
681
681
|
gitignoreContent += '\n';
|
|
682
682
|
}
|
|
683
683
|
|
|
684
|
-
//
|
|
684
|
+
// Add the missing entries
|
|
685
685
|
gitignoreContent += missingEntries.join('\n') + '\n';
|
|
686
686
|
|
|
687
|
-
//
|
|
687
|
+
// Write the updated file
|
|
688
688
|
fs.writeFileSync(gitignorePath, gitignoreContent);
|
|
689
689
|
|
|
690
690
|
if (fileExists) {
|
|
691
|
-
success('.gitignore
|
|
691
|
+
success('.gitignore updated with Claude entries');
|
|
692
692
|
} else {
|
|
693
|
-
success('.gitignore
|
|
693
|
+
success('.gitignore created with Claude entries');
|
|
694
694
|
}
|
|
695
695
|
|
|
696
|
-
//
|
|
696
|
+
// Show what was added
|
|
697
697
|
missingEntries.forEach(entry => {
|
|
698
698
|
if (!entry.startsWith('#')) {
|
|
699
699
|
info(` + ${entry}`);
|
|
700
700
|
}
|
|
701
701
|
});
|
|
702
702
|
} else {
|
|
703
|
-
info('.gitignore
|
|
703
|
+
info('.gitignore already contains all necessary entries');
|
|
704
704
|
}
|
|
705
705
|
}
|
|
706
706
|
|
|
707
|
-
//
|
|
707
|
+
// Configure Git (line endings, etc.)
|
|
708
708
|
function configureGit() {
|
|
709
|
-
info('
|
|
709
|
+
info('Configuring Git...');
|
|
710
710
|
|
|
711
711
|
try {
|
|
712
|
-
//
|
|
712
|
+
// Configure line endings for WSL
|
|
713
713
|
execSync('git config core.autocrlf input', { stdio: 'ignore' });
|
|
714
|
-
success('Line endings
|
|
714
|
+
success('Line endings configured for WSL (core.autocrlf = input)');
|
|
715
715
|
|
|
716
|
-
//
|
|
716
|
+
// Try to configure on Windows through PowerShell
|
|
717
717
|
try {
|
|
718
718
|
execSync('powershell.exe -Command "git config core.autocrlf true"', { stdio: 'ignore' });
|
|
719
|
-
success('Line endings
|
|
719
|
+
success('Line endings configured for Windows (core.autocrlf = true)');
|
|
720
720
|
} catch (psError) {
|
|
721
|
-
info('
|
|
721
|
+
info('Could not configure automatically on Windows');
|
|
722
722
|
}
|
|
723
723
|
|
|
724
724
|
} catch (e) {
|
|
725
|
-
warning('Error
|
|
725
|
+
warning('Error configuring Git');
|
|
726
726
|
}
|
|
727
727
|
}
|
|
728
728
|
|
|
729
|
-
//
|
|
729
|
+
// Uninstall command
|
|
730
730
|
function uninstall() {
|
|
731
731
|
if (!checkGitRepo()) {
|
|
732
|
-
error('
|
|
732
|
+
error('You are not in a Git repository.');
|
|
733
733
|
}
|
|
734
734
|
|
|
735
|
-
info('
|
|
735
|
+
info('Uninstalling Claude Git Hooks...');
|
|
736
736
|
|
|
737
737
|
const hooksPath = '.git/hooks';
|
|
738
738
|
const hooks = ['pre-commit', 'prepare-commit-msg'];
|
|
@@ -741,17 +741,17 @@ function uninstall() {
|
|
|
741
741
|
const hookPath = path.join(hooksPath, hook);
|
|
742
742
|
if (fs.existsSync(hookPath)) {
|
|
743
743
|
fs.unlinkSync(hookPath);
|
|
744
|
-
success(`${hook}
|
|
744
|
+
success(`${hook} removed`);
|
|
745
745
|
}
|
|
746
746
|
});
|
|
747
747
|
|
|
748
|
-
success('Claude Git Hooks
|
|
748
|
+
success('Claude Git Hooks uninstalled');
|
|
749
749
|
}
|
|
750
750
|
|
|
751
|
-
//
|
|
751
|
+
// Enable command
|
|
752
752
|
function enable(hookName) {
|
|
753
753
|
if (!checkGitRepo()) {
|
|
754
|
-
error('
|
|
754
|
+
error('You are not in a Git repository.');
|
|
755
755
|
}
|
|
756
756
|
|
|
757
757
|
const hooks = hookName ? [hookName] : ['pre-commit', 'prepare-commit-msg'];
|
|
@@ -762,19 +762,19 @@ function enable(hookName) {
|
|
|
762
762
|
|
|
763
763
|
if (fs.existsSync(disabledPath)) {
|
|
764
764
|
fs.renameSync(disabledPath, enabledPath);
|
|
765
|
-
success(`${hook}
|
|
765
|
+
success(`${hook} enabled`);
|
|
766
766
|
} else if (fs.existsSync(enabledPath)) {
|
|
767
|
-
info(`${hook}
|
|
767
|
+
info(`${hook} is already enabled`);
|
|
768
768
|
} else {
|
|
769
|
-
warning(`${hook}
|
|
769
|
+
warning(`${hook} not found`);
|
|
770
770
|
}
|
|
771
771
|
});
|
|
772
772
|
}
|
|
773
773
|
|
|
774
|
-
//
|
|
774
|
+
// Disable command
|
|
775
775
|
function disable(hookName) {
|
|
776
776
|
if (!checkGitRepo()) {
|
|
777
|
-
error('
|
|
777
|
+
error('You are not in a Git repository.');
|
|
778
778
|
}
|
|
779
779
|
|
|
780
780
|
const hooks = hookName ? [hookName] : ['pre-commit', 'prepare-commit-msg'];
|
|
@@ -785,67 +785,67 @@ function disable(hookName) {
|
|
|
785
785
|
|
|
786
786
|
if (fs.existsSync(enabledPath)) {
|
|
787
787
|
fs.renameSync(enabledPath, disabledPath);
|
|
788
|
-
success(`${hook}
|
|
788
|
+
success(`${hook} disabled`);
|
|
789
789
|
} else if (fs.existsSync(disabledPath)) {
|
|
790
|
-
info(`${hook}
|
|
790
|
+
info(`${hook} is already disabled`);
|
|
791
791
|
} else {
|
|
792
|
-
warning(`${hook}
|
|
792
|
+
warning(`${hook} not found`);
|
|
793
793
|
}
|
|
794
794
|
});
|
|
795
795
|
}
|
|
796
796
|
|
|
797
|
-
//
|
|
797
|
+
// Analyze-diff command
|
|
798
798
|
function analyzeDiff(args) {
|
|
799
799
|
if (!checkGitRepo()) {
|
|
800
|
-
error('
|
|
800
|
+
error('You are not in a Git repository.');
|
|
801
801
|
return;
|
|
802
802
|
}
|
|
803
803
|
|
|
804
804
|
const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
|
|
805
805
|
|
|
806
806
|
if (!currentBranch) {
|
|
807
|
-
error('
|
|
807
|
+
error('You are not in a valid branch.');
|
|
808
808
|
return;
|
|
809
809
|
}
|
|
810
810
|
|
|
811
811
|
let baseBranch, compareWith, contextDescription;
|
|
812
812
|
|
|
813
813
|
if (args[0]) {
|
|
814
|
-
//
|
|
814
|
+
// Case with argument: compare current branch vs specified branch
|
|
815
815
|
baseBranch = args[0];
|
|
816
816
|
compareWith = `${baseBranch}...HEAD`;
|
|
817
817
|
contextDescription = `${currentBranch} vs ${baseBranch}`;
|
|
818
818
|
|
|
819
|
-
//
|
|
819
|
+
// Check that the base branch exists
|
|
820
820
|
try {
|
|
821
821
|
execSync(`git rev-parse --verify ${baseBranch}`, { stdio: 'ignore' });
|
|
822
822
|
} catch (e) {
|
|
823
|
-
error(`
|
|
823
|
+
error(`Branch ${baseBranch} does not exist.`);
|
|
824
824
|
return;
|
|
825
825
|
}
|
|
826
826
|
} else {
|
|
827
|
-
//
|
|
827
|
+
// Case without argument: compare local vs remote
|
|
828
828
|
try {
|
|
829
|
-
//
|
|
829
|
+
// Get the tracked remote branch
|
|
830
830
|
const remoteBranch = execSync(`git rev-parse --abbrev-ref ${currentBranch}@{upstream}`, { encoding: 'utf8' }).trim();
|
|
831
831
|
|
|
832
|
-
//
|
|
832
|
+
// Update remote references
|
|
833
833
|
execSync('git fetch', { stdio: 'ignore' });
|
|
834
834
|
|
|
835
835
|
baseBranch = remoteBranch;
|
|
836
836
|
compareWith = `HEAD..${remoteBranch}`;
|
|
837
|
-
contextDescription = `
|
|
837
|
+
contextDescription = `local changes without push in ${currentBranch}`;
|
|
838
838
|
|
|
839
|
-
info(`
|
|
839
|
+
info(`Comparing local changes vs remote: ${remoteBranch}`);
|
|
840
840
|
} catch (e) {
|
|
841
|
-
//
|
|
841
|
+
// If there's no remote branch, use develop as fallback
|
|
842
842
|
baseBranch = 'develop';
|
|
843
843
|
compareWith = `${baseBranch}...HEAD`;
|
|
844
844
|
contextDescription = `${currentBranch} vs ${baseBranch} (sin rama remota)`;
|
|
845
845
|
|
|
846
846
|
try {
|
|
847
847
|
execSync(`git rev-parse --verify ${baseBranch}`, { stdio: 'ignore' });
|
|
848
|
-
warning(`No
|
|
848
|
+
warning(`No remote branch configured. Comparing with ${baseBranch}.`);
|
|
849
849
|
} catch (e2) {
|
|
850
850
|
baseBranch = 'main';
|
|
851
851
|
compareWith = `${baseBranch}...HEAD`;
|
|
@@ -853,21 +853,21 @@ function analyzeDiff(args) {
|
|
|
853
853
|
|
|
854
854
|
try {
|
|
855
855
|
execSync(`git rev-parse --verify ${baseBranch}`, { stdio: 'ignore' });
|
|
856
|
-
warning(`No
|
|
856
|
+
warning(`No develop or remote branch. Comparing with main.`);
|
|
857
857
|
} catch (e3) {
|
|
858
|
-
error('
|
|
858
|
+
error('Could not find a valid comparison branch.');
|
|
859
859
|
return;
|
|
860
860
|
}
|
|
861
861
|
}
|
|
862
862
|
}
|
|
863
863
|
}
|
|
864
864
|
|
|
865
|
-
info(`
|
|
865
|
+
info(`Analyzing: ${contextDescription}...`);
|
|
866
866
|
|
|
867
|
-
//
|
|
867
|
+
// Get modified files
|
|
868
868
|
let diffFiles;
|
|
869
869
|
try {
|
|
870
|
-
//
|
|
870
|
+
// For local changes without push, invert the comparison
|
|
871
871
|
if (!args[0] && compareWith.includes('HEAD..')) {
|
|
872
872
|
diffFiles = execSync(`git diff ${compareWith} --name-only`, { encoding: 'utf8' }).trim();
|
|
873
873
|
} else {
|
|
@@ -875,25 +875,25 @@ function analyzeDiff(args) {
|
|
|
875
875
|
}
|
|
876
876
|
|
|
877
877
|
if (!diffFiles) {
|
|
878
|
-
//
|
|
878
|
+
// Check if there are staged or unstaged changes
|
|
879
879
|
const stagedFiles = execSync('git diff --cached --name-only', { encoding: 'utf8' }).trim();
|
|
880
880
|
const unstagedFiles = execSync('git diff --name-only', { encoding: 'utf8' }).trim();
|
|
881
881
|
|
|
882
882
|
if (stagedFiles || unstagedFiles) {
|
|
883
|
-
warning('No
|
|
884
|
-
console.log('
|
|
885
|
-
console.log('
|
|
883
|
+
warning('No differences with remote, but you have uncommitted local changes.');
|
|
884
|
+
console.log('Staged changes:', stagedFiles || 'none');
|
|
885
|
+
console.log('Unstaged changes:', unstagedFiles || 'none');
|
|
886
886
|
} else {
|
|
887
|
-
success('✅ No
|
|
887
|
+
success('✅ No differences. Your branch is synchronized.');
|
|
888
888
|
}
|
|
889
889
|
return;
|
|
890
890
|
}
|
|
891
891
|
} catch (e) {
|
|
892
|
-
error('Error
|
|
892
|
+
error('Error getting differences: ' + e.message);
|
|
893
893
|
return;
|
|
894
894
|
}
|
|
895
895
|
|
|
896
|
-
//
|
|
896
|
+
// Get the complete diff
|
|
897
897
|
let fullDiff, commits;
|
|
898
898
|
try {
|
|
899
899
|
if (!args[0] && compareWith.includes('HEAD..')) {
|
|
@@ -904,50 +904,50 @@ function analyzeDiff(args) {
|
|
|
904
904
|
commits = execSync(`git log ${baseBranch}..HEAD --oneline`, { encoding: 'utf8' }).trim();
|
|
905
905
|
}
|
|
906
906
|
} catch (e) {
|
|
907
|
-
error('Error
|
|
907
|
+
error('Error getting diff or commits: ' + e.message);
|
|
908
908
|
return;
|
|
909
909
|
}
|
|
910
910
|
|
|
911
|
-
//
|
|
911
|
+
// Create the prompt for Claude
|
|
912
912
|
const tempDir = `/tmp/claude-analyze-${Date.now()}`;
|
|
913
913
|
fs.mkdirSync(tempDir, { recursive: true });
|
|
914
914
|
|
|
915
915
|
const promptFile = path.join(tempDir, 'prompt.txt');
|
|
916
|
-
const prompt = `
|
|
916
|
+
const prompt = `Analyze the following changes. CONTEXT: ${contextDescription}
|
|
917
917
|
|
|
918
|
-
|
|
919
|
-
1.
|
|
920
|
-
2.
|
|
921
|
-
-
|
|
922
|
-
-
|
|
923
|
-
-
|
|
924
|
-
-
|
|
925
|
-
3.
|
|
918
|
+
Please generate:
|
|
919
|
+
1. A concise and descriptive PR title (maximum 72 characters)
|
|
920
|
+
2. A detailed PR description that includes:
|
|
921
|
+
- Summary of changes
|
|
922
|
+
- Motivation/context
|
|
923
|
+
- Type of change (feature/fix/refactor/docs/etc)
|
|
924
|
+
- Recommended testing
|
|
925
|
+
3. A suggested branch name following the format: type/short-description (example: feature/add-user-auth, fix/memory-leak)
|
|
926
926
|
|
|
927
|
-
|
|
927
|
+
IMPORTANT: If these are local changes without push, the suggested branch name should be for creating a new branch from the current one.
|
|
928
928
|
|
|
929
|
-
|
|
929
|
+
Respond EXCLUSIVELY with a valid JSON with this structure:
|
|
930
930
|
{
|
|
931
|
-
"prTitle": "
|
|
932
|
-
"prDescription": "
|
|
933
|
-
"suggestedBranchName": "
|
|
931
|
+
"prTitle": "Interesting PR title",
|
|
932
|
+
"prDescription": "detailed PR description with markdown",
|
|
933
|
+
"suggestedBranchName": "type/suggested-branch-name",
|
|
934
934
|
"changeType": "feature|fix|refactor|docs|test|chore",
|
|
935
935
|
"breakingChanges": false,
|
|
936
|
-
"testingNotes": "
|
|
936
|
+
"testingNotes": "notes on necessary testing or 'None'"
|
|
937
937
|
}
|
|
938
938
|
|
|
939
939
|
=== COMMITS ===
|
|
940
940
|
${commits}
|
|
941
941
|
|
|
942
|
-
===
|
|
942
|
+
=== CHANGED FILES ===
|
|
943
943
|
${diffFiles}
|
|
944
944
|
|
|
945
|
-
=== DIFF
|
|
946
|
-
${fullDiff.substring(0, 50000)} ${fullDiff.length > 50000 ? '\n... (diff
|
|
945
|
+
=== FULL DIFF ===
|
|
946
|
+
${fullDiff.substring(0, 50000)} ${fullDiff.length > 50000 ? '\n... (truncated diff)' : ''}`;
|
|
947
947
|
|
|
948
948
|
fs.writeFileSync(promptFile, prompt);
|
|
949
949
|
|
|
950
|
-
info('
|
|
950
|
+
info('Sending to Claude for analysis...');
|
|
951
951
|
|
|
952
952
|
try {
|
|
953
953
|
const response = execSync(`claude < "${promptFile}"`, { encoding: 'utf8', maxBuffer: 1024 * 1024 * 10 });
|
|
@@ -955,8 +955,8 @@ ${fullDiff.substring(0, 50000)} ${fullDiff.length > 50000 ? '\n... (diff truncad
|
|
|
955
955
|
// Extraer el JSON de la respuesta
|
|
956
956
|
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
957
957
|
if (!jsonMatch) {
|
|
958
|
-
error('
|
|
959
|
-
console.log('
|
|
958
|
+
error('Did not receive a valid JSON response from Claude.');
|
|
959
|
+
console.log('Complete response:', response);
|
|
960
960
|
return;
|
|
961
961
|
}
|
|
962
962
|
|
|
@@ -964,45 +964,45 @@ ${fullDiff.substring(0, 50000)} ${fullDiff.length > 50000 ? '\n... (diff truncad
|
|
|
964
964
|
try {
|
|
965
965
|
result = JSON.parse(jsonMatch[0]);
|
|
966
966
|
} catch (e) {
|
|
967
|
-
error('Error
|
|
968
|
-
console.log('JSON
|
|
967
|
+
error('Error parsing JSON response: ' + e.message);
|
|
968
|
+
console.log('JSON received:', jsonMatch[0]);
|
|
969
969
|
return;
|
|
970
970
|
}
|
|
971
971
|
|
|
972
|
-
//
|
|
972
|
+
// Show the results
|
|
973
973
|
console.log('');
|
|
974
|
-
console.log('
|
|
975
|
-
console.log('
|
|
976
|
-
console.log('
|
|
974
|
+
console.log('════════════════════════════════════════════════════════════════');
|
|
975
|
+
console.log(' DIFFERENCES ANALYSIS ');
|
|
976
|
+
console.log('════════════════════════════════════════════════════════════════');
|
|
977
977
|
console.log('');
|
|
978
978
|
|
|
979
|
-
console.log(`🔍 ${colors.blue}
|
|
980
|
-
console.log(`📊 ${colors.blue}
|
|
979
|
+
console.log(`🔍 ${colors.blue}Context:${colors.reset} ${contextDescription}`);
|
|
980
|
+
console.log(`📊 ${colors.blue}Changed Files:${colors.reset} ${diffFiles.split('\n').length}`);
|
|
981
981
|
console.log('');
|
|
982
982
|
|
|
983
|
-
console.log(`📝 ${colors.green}
|
|
983
|
+
console.log(`📝 ${colors.green}Pull Request Title:${colors.reset}`);
|
|
984
984
|
console.log(` ${result.prTitle}`);
|
|
985
985
|
console.log('');
|
|
986
986
|
|
|
987
|
-
console.log(`🌿 ${colors.green}
|
|
987
|
+
console.log(`🌿 ${colors.green}Suggested branch name:${colors.reset}`);
|
|
988
988
|
console.log(` ${result.suggestedBranchName}`);
|
|
989
989
|
console.log('');
|
|
990
990
|
|
|
991
|
-
console.log(`📋 ${colors.green}
|
|
991
|
+
console.log(`📋 ${colors.green}Type of change:${colors.reset} ${result.changeType}`);
|
|
992
992
|
|
|
993
993
|
if (result.breakingChanges) {
|
|
994
994
|
console.log(`⚠️ ${colors.yellow}Breaking Changes: SÍ${colors.reset}`);
|
|
995
995
|
}
|
|
996
996
|
|
|
997
997
|
console.log('');
|
|
998
|
-
console.log(`📄 ${colors.green}
|
|
998
|
+
console.log(`📄 ${colors.green}Pull Request Description:${colors.reset}`);
|
|
999
999
|
console.log('───────────────────────────────────────────────────────────────');
|
|
1000
1000
|
console.log(result.prDescription);
|
|
1001
1001
|
console.log('───────────────────────────────────────────────────────────────');
|
|
1002
1002
|
|
|
1003
1003
|
if (result.testingNotes) {
|
|
1004
1004
|
console.log('');
|
|
1005
|
-
console.log(`🧪 ${colors.green}
|
|
1005
|
+
console.log(`🧪 ${colors.green}Testing notes:${colors.reset}`);
|
|
1006
1006
|
console.log(result.testingNotes);
|
|
1007
1007
|
}
|
|
1008
1008
|
|
|
@@ -1021,27 +1021,27 @@ ${fullDiff.substring(0, 50000)} ${fullDiff.length > 50000 ? '\n... (diff truncad
|
|
|
1021
1021
|
const outputFile = '.claude-pr-analysis.json';
|
|
1022
1022
|
fs.writeFileSync(outputFile, JSON.stringify(outputData, null, 2));
|
|
1023
1023
|
console.log('');
|
|
1024
|
-
info(`
|
|
1024
|
+
info(`Results saved in ${outputFile}`);
|
|
1025
1025
|
|
|
1026
1026
|
// Sugerencias contextuales
|
|
1027
1027
|
console.log('');
|
|
1028
|
-
if (!args[0] && contextDescription.includes('
|
|
1029
|
-
//
|
|
1030
|
-
console.log(`💡 ${colors.yellow}
|
|
1028
|
+
if (!args[0] && contextDescription.includes('local changes without push')) {
|
|
1029
|
+
// Case of local changes without push
|
|
1030
|
+
console.log(`💡 ${colors.yellow}To create new branch with these changes:${colors.reset}`);
|
|
1031
1031
|
console.log(` git checkout -b ${result.suggestedBranchName}`);
|
|
1032
1032
|
console.log(` git push -u origin ${result.suggestedBranchName}`);
|
|
1033
1033
|
} else if (currentBranch !== result.suggestedBranchName) {
|
|
1034
1034
|
// Caso normal de comparación entre ramas
|
|
1035
|
-
console.log(`💡 ${colors.yellow}
|
|
1035
|
+
console.log(`💡 ${colors.yellow}For renaming your current branch:${colors.reset}`);
|
|
1036
1036
|
console.log(` git branch -m ${result.suggestedBranchName}`);
|
|
1037
1037
|
}
|
|
1038
1038
|
|
|
1039
|
-
console.log(`💡 ${colors.yellow}Tip:${colors.reset}
|
|
1039
|
+
console.log(`💡 ${colors.yellow}Tip:${colors.reset} Use this information to create your PR on GitHub.`);
|
|
1040
1040
|
|
|
1041
1041
|
} catch (e) {
|
|
1042
|
-
error('Error
|
|
1042
|
+
error('Error executing Claude: ' + e.message);
|
|
1043
1043
|
} finally {
|
|
1044
|
-
//
|
|
1044
|
+
// Clean temporary files
|
|
1045
1045
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
1046
1046
|
}
|
|
1047
1047
|
}
|
|
@@ -1049,10 +1049,10 @@ ${fullDiff.substring(0, 50000)} ${fullDiff.length > 50000 ? '\n... (diff truncad
|
|
|
1049
1049
|
// Comando status
|
|
1050
1050
|
function status() {
|
|
1051
1051
|
if (!checkGitRepo()) {
|
|
1052
|
-
error('
|
|
1052
|
+
error('You are not in a Git repository.');
|
|
1053
1053
|
}
|
|
1054
1054
|
|
|
1055
|
-
info('
|
|
1055
|
+
info('Claude Git Hooks status:\n');
|
|
1056
1056
|
|
|
1057
1057
|
const hooks = ['pre-commit', 'prepare-commit-msg'];
|
|
1058
1058
|
hooks.forEach(hook => {
|
|
@@ -1060,25 +1060,25 @@ function status() {
|
|
|
1060
1060
|
const disabledPath = `.git/hooks/${hook}.disabled`;
|
|
1061
1061
|
|
|
1062
1062
|
if (fs.existsSync(enabledPath)) {
|
|
1063
|
-
success(`${hook}:
|
|
1063
|
+
success(`${hook}: enabled`);
|
|
1064
1064
|
} else if (fs.existsSync(disabledPath)) {
|
|
1065
|
-
warning(`${hook}:
|
|
1065
|
+
warning(`${hook}: disabled`);
|
|
1066
1066
|
} else {
|
|
1067
|
-
error(`${hook}:
|
|
1067
|
+
error(`${hook}: not installed`);
|
|
1068
1068
|
}
|
|
1069
1069
|
});
|
|
1070
1070
|
|
|
1071
|
-
//
|
|
1072
|
-
console.log('\
|
|
1071
|
+
// Check guidelines files
|
|
1072
|
+
console.log('\nGuidelines files:');
|
|
1073
1073
|
const guidelines = ['CLAUDE_PRE_COMMIT_SONAR.md'];
|
|
1074
1074
|
guidelines.forEach(guideline => {
|
|
1075
1075
|
const claudePath = path.join('.claude', guideline);
|
|
1076
1076
|
if (fs.existsSync(claudePath)) {
|
|
1077
|
-
success(`${guideline}:
|
|
1077
|
+
success(`${guideline}: present in .claude/`);
|
|
1078
1078
|
} else if (fs.existsSync(guideline)) {
|
|
1079
|
-
warning(`${guideline}:
|
|
1079
|
+
warning(`${guideline}: present in root (should be in .claude/)`);
|
|
1080
1080
|
} else {
|
|
1081
|
-
warning(`${guideline}:
|
|
1081
|
+
warning(`${guideline}: missing`);
|
|
1082
1082
|
}
|
|
1083
1083
|
});
|
|
1084
1084
|
|
|
@@ -1093,25 +1093,25 @@ function status() {
|
|
|
1093
1093
|
claudeIgnores.forEach(entry => {
|
|
1094
1094
|
const regex = new RegExp(`^${entry.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm');
|
|
1095
1095
|
if (regex.test(gitignoreContent)) {
|
|
1096
|
-
success(`${entry}:
|
|
1096
|
+
success(`${entry}: included`);
|
|
1097
1097
|
} else {
|
|
1098
|
-
warning(`${entry}:
|
|
1098
|
+
warning(`${entry}: missing`);
|
|
1099
1099
|
allPresent = false;
|
|
1100
1100
|
}
|
|
1101
1101
|
});
|
|
1102
1102
|
|
|
1103
1103
|
if (!allPresent) {
|
|
1104
|
-
info('\
|
|
1104
|
+
info('\nRun "claude-hooks install" to update .gitignore');
|
|
1105
1105
|
}
|
|
1106
1106
|
} else {
|
|
1107
|
-
warning('.gitignore
|
|
1107
|
+
warning('.gitignore doesn´t exist');
|
|
1108
1108
|
}
|
|
1109
1109
|
}
|
|
1110
1110
|
|
|
1111
1111
|
// Comando set-mode
|
|
1112
1112
|
function setMode(mode) {
|
|
1113
1113
|
if (!checkGitRepo()) {
|
|
1114
|
-
error('
|
|
1114
|
+
error('You are not in a Git repository.');
|
|
1115
1115
|
}
|
|
1116
1116
|
}
|
|
1117
1117
|
|
|
@@ -1147,9 +1147,9 @@ function compareVersions(v1, v2) {
|
|
|
1147
1147
|
}
|
|
1148
1148
|
}
|
|
1149
1149
|
|
|
1150
|
-
//
|
|
1150
|
+
// Update command - update to the latest version
|
|
1151
1151
|
async function update() {
|
|
1152
|
-
info('
|
|
1152
|
+
info('Checking latest available version...');
|
|
1153
1153
|
|
|
1154
1154
|
try {
|
|
1155
1155
|
const currentVersion = require('../package.json').version;
|
|
@@ -1158,40 +1158,40 @@ async function update() {
|
|
|
1158
1158
|
const comparison = compareVersions(currentVersion, latestVersion);
|
|
1159
1159
|
|
|
1160
1160
|
if (comparison === 0) {
|
|
1161
|
-
success(`
|
|
1161
|
+
success(`You already have the latest version installed (${currentVersion})`);
|
|
1162
1162
|
return;
|
|
1163
1163
|
} else if (comparison > 0) {
|
|
1164
|
-
info(`
|
|
1165
|
-
info(
|
|
1166
|
-
success(`
|
|
1164
|
+
info(`You are using a development version (${currentVersion})`);
|
|
1165
|
+
info(`Latest published version: ${latestVersion}`);
|
|
1166
|
+
success(`You already have the latest version installed (${currentVersion})`);
|
|
1167
1167
|
return;
|
|
1168
1168
|
}
|
|
1169
1169
|
|
|
1170
|
-
info(`
|
|
1171
|
-
info(`
|
|
1170
|
+
info(`Current version: ${currentVersion}`);
|
|
1171
|
+
info(`Available version: ${latestVersion}`);
|
|
1172
1172
|
|
|
1173
1173
|
// Actualizar el paquete
|
|
1174
|
-
info('
|
|
1174
|
+
info('Updating claude-git-hooks...');
|
|
1175
1175
|
try {
|
|
1176
1176
|
execSync('npm install -g claude-git-hooks@latest', { stdio: 'inherit' });
|
|
1177
|
-
success(`
|
|
1177
|
+
success(`Successfully updated to version ${latestVersion}`);
|
|
1178
1178
|
|
|
1179
|
-
//
|
|
1180
|
-
info('
|
|
1179
|
+
// Reinstall hooks with the new version
|
|
1180
|
+
info('Reinstalling hooks with the new version...');
|
|
1181
1181
|
await install(['--force']);
|
|
1182
1182
|
|
|
1183
1183
|
} catch (updateError) {
|
|
1184
|
-
error('Error
|
|
1184
|
+
error('Error updating. Try running: npm install -g claude-git-hooks@latest');
|
|
1185
1185
|
}
|
|
1186
1186
|
} catch (e) {
|
|
1187
|
-
warning('
|
|
1188
|
-
warning('
|
|
1187
|
+
warning('Could not check the latest available version');
|
|
1188
|
+
warning('Trying to update anyway...');
|
|
1189
1189
|
try {
|
|
1190
1190
|
execSync('npm install -g claude-git-hooks@latest', { stdio: 'inherit' });
|
|
1191
|
-
success('
|
|
1191
|
+
success('Update completed');
|
|
1192
1192
|
await install(['--force']);
|
|
1193
1193
|
} catch (updateError) {
|
|
1194
|
-
error('Error
|
|
1194
|
+
error('Error updating: ' + updateError.message);
|
|
1195
1195
|
}
|
|
1196
1196
|
}
|
|
1197
1197
|
}
|
|
@@ -1199,54 +1199,54 @@ async function update() {
|
|
|
1199
1199
|
// Comando help
|
|
1200
1200
|
function showHelp() {
|
|
1201
1201
|
console.log(`
|
|
1202
|
-
Claude Git Hooks -
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
install [
|
|
1208
|
-
--force
|
|
1209
|
-
--skip-auth
|
|
1210
|
-
update
|
|
1211
|
-
uninstall
|
|
1212
|
-
enable [hook]
|
|
1213
|
-
disable [hook]
|
|
1214
|
-
status
|
|
1215
|
-
analyze-diff [base]
|
|
1216
|
-
help
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
pre-commit
|
|
1220
|
-
prepare-commit-msg
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
claude-hooks install #
|
|
1224
|
-
claude-hooks install --skip-auth #
|
|
1225
|
-
claude-hooks update #
|
|
1226
|
-
claude-hooks disable pre-commit #
|
|
1227
|
-
claude-hooks enable #
|
|
1228
|
-
claude-hooks status #
|
|
1229
|
-
claude-hooks analyze-diff main #
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
git commit -m "
|
|
1233
|
-
git commit -m "auto" #
|
|
1234
|
-
git commit --no-verify -m "auto" #
|
|
1235
|
-
git commit --no-verify -m "msg" #
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
claude-hooks analyze-diff main #
|
|
1239
|
-
→
|
|
1240
|
-
→
|
|
1241
|
-
→
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
// SKIP-ANALYSIS #
|
|
1245
|
-
// SKIP_ANALYSIS_BLOCK #
|
|
1246
|
-
...
|
|
1202
|
+
Claude Git Hooks - Code analysis and automatic messages with Claude CLI
|
|
1203
|
+
|
|
1204
|
+
Usage: claude-hooks <command> [options]
|
|
1205
|
+
|
|
1206
|
+
Commands:
|
|
1207
|
+
install [options] Install hooks in the current repository
|
|
1208
|
+
--force Reinstall even if they already exist
|
|
1209
|
+
--skip-auth Skip Claude authentication verification
|
|
1210
|
+
update Update to the latest available version
|
|
1211
|
+
uninstall Uninstall hooks from the repository
|
|
1212
|
+
enable [hook] Enable hooks (all or one specific)
|
|
1213
|
+
disable [hook] Disable hooks (all or one specific)
|
|
1214
|
+
status Show the status of hooks
|
|
1215
|
+
analyze-diff [base] Analyze differences between branches and generate PR info
|
|
1216
|
+
help Show this help
|
|
1217
|
+
|
|
1218
|
+
Available hooks:
|
|
1219
|
+
pre-commit Code analysis before commit
|
|
1220
|
+
prepare-commit-msg Automatic message generation
|
|
1221
|
+
|
|
1222
|
+
Examples:
|
|
1223
|
+
claude-hooks install # Install all hooks
|
|
1224
|
+
claude-hooks install --skip-auth # Install without verifying authentication
|
|
1225
|
+
claude-hooks update # Update to the latest version
|
|
1226
|
+
claude-hooks disable pre-commit # Disable only pre-commit
|
|
1227
|
+
claude-hooks enable # Enable all hooks
|
|
1228
|
+
claude-hooks status # View current status
|
|
1229
|
+
claude-hooks analyze-diff main # Analyze differences with main
|
|
1230
|
+
|
|
1231
|
+
Commit use cases:
|
|
1232
|
+
git commit -m "message" # Manual message + blocking analysis
|
|
1233
|
+
git commit -m "auto" # Automatic message + blocking analysis
|
|
1234
|
+
git commit --no-verify -m "auto" # Automatic message without analysis
|
|
1235
|
+
git commit --no-verify -m "msg" # Manual message without analysis
|
|
1236
|
+
|
|
1237
|
+
Analyze-diff use case:
|
|
1238
|
+
claude-hooks analyze-diff main # Analyze changes vs main and generate:
|
|
1239
|
+
→ PR Title: "feat: add user authentication module"
|
|
1240
|
+
→ PR Description: "## Summary\n- Added JWT authentication..."
|
|
1241
|
+
→ Suggested branch: "feature/user-authentication"
|
|
1242
|
+
|
|
1243
|
+
Exclude code from analysis:
|
|
1244
|
+
// SKIP-ANALYSIS # Exclude the next line from analysis
|
|
1245
|
+
// SKIP_ANALYSIS_BLOCK # Exclude block until finding another equal one
|
|
1246
|
+
...excluded code...
|
|
1247
1247
|
// SKIP_ANALYSIS_BLOCK
|
|
1248
1248
|
|
|
1249
|
-
|
|
1249
|
+
More information: https://github.com/pablorovito/claude-git-hooks
|
|
1250
1250
|
`);
|
|
1251
1251
|
}
|
|
1252
1252
|
|
|
@@ -1284,12 +1284,12 @@ async function main() {
|
|
|
1284
1284
|
showHelp();
|
|
1285
1285
|
break;
|
|
1286
1286
|
default:
|
|
1287
|
-
error(`
|
|
1287
|
+
error(`Unknown command: ${command}`);
|
|
1288
1288
|
showHelp();
|
|
1289
1289
|
}
|
|
1290
1290
|
}
|
|
1291
1291
|
|
|
1292
|
-
//
|
|
1292
|
+
// Execute main
|
|
1293
1293
|
main().catch(err => {
|
|
1294
|
-
error(`
|
|
1294
|
+
error(`Unexpected error: ${err.message}`);
|
|
1295
1295
|
});
|