claude-git-hooks 1.5.5 → 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/bin/claude-hooks CHANGED
@@ -1,11 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { execSync, spawn } = require('child_process');
4
- const fs = require('fs');
5
- const path = require('path');
6
- const os = require('os');
7
- const readline = require('readline');
8
- const https = require('https');
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,7 +54,7 @@ function getLatestVersion(packageName) {
40
54
  // Function to check version (used by hooks)
41
55
  async function checkVersionAndPromptUpdate() {
42
56
  try {
43
- const currentVersion = require('../package.json').version;
57
+ const currentVersion = getPackageJson().version;
44
58
  const latestVersion = await getLatestVersion('claude-git-hooks');
45
59
 
46
60
  if (currentVersion === latestVersion) {
@@ -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',
@@ -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 = [
@@ -368,25 +338,9 @@ async function install(args) {
368
338
  info('Installing Claude Git Hooks...');
369
339
  }
370
340
 
371
- // Request sudo password at the beginning if necessary
372
- let sudoPassword = null;
373
- if (os.platform() === 'linux') {
374
- const needsInstall = await checkIfInstallationNeeded();
375
- if (needsInstall) {
376
- info('Sudo access is needed for automatic dependency installation, please enter password');
377
- sudoPassword = await readPassword('Enter your Ubuntu password for sudo: ');
378
-
379
- if (sudoPassword && !testSudoPassword(sudoPassword)) {
380
- warning('Incorrect password. Continuing without automatic installation.');
381
- sudoPassword = null;
382
- } else if (sudoPassword) {
383
- success('Password verified. Proceeding with automatic installation.');
384
- }
385
- }
386
- }
387
-
388
- // Check dependencies with automatic installation
389
- 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);
390
344
 
391
345
  const templatesPath = getTemplatesPath();
392
346
  const hooksPath = '.git/hooks';
@@ -396,6 +350,15 @@ async function install(args) {
396
350
  fs.mkdirSync(hooksPath, { recursive: true });
397
351
  }
398
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
+
399
362
  // Hooks to install
400
363
  const hooks = ['pre-commit', 'prepare-commit-msg'];
401
364
 
@@ -410,18 +373,18 @@ async function install(args) {
410
373
  info(`Backup created: ${backupPath}`);
411
374
  }
412
375
 
413
- // Copy hook
414
- fs.copyFileSync(sourcePath, destPath);
376
+ // Copy hook with LF line endings (critical for bash)
377
+ copyWithLF(sourcePath, destPath);
415
378
  fs.chmodSync(destPath, '755');
416
379
  success(`${hook} installed`);
417
380
  });
418
381
 
419
- // Copy version verification script
382
+ // Copy version verification script with LF line endings
420
383
  const checkVersionSource = path.join(templatesPath, 'check-version.sh');
421
384
  const checkVersionDest = path.join(hooksPath, 'check-version.sh');
422
385
 
423
386
  if (fs.existsSync(checkVersionSource)) {
424
- fs.copyFileSync(checkVersionSource, checkVersionDest);
387
+ copyWithLF(checkVersionSource, checkVersionDest);
425
388
  fs.chmodSync(checkVersionDest, '755');
426
389
  success('Version verification script installed');
427
390
  }
@@ -500,41 +463,7 @@ async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false
500
463
  error('npm is not installed.');
501
464
  }
502
465
 
503
- // Check and install jq
504
- try {
505
- const jqVersion = execSync('jq --version', { encoding: 'utf8' }).trim();
506
- success(`jq ${jqVersion}`);
507
- } catch (e) {
508
- warning('jq is not installed. Installing...');
509
- if (installPackage('jq', sudoPassword)) {
510
- success('jq installed successfully');
511
- } else {
512
- warning('Could not install jq automatically');
513
- if (os.platform() === 'linux') {
514
- console.log('Install it manually with: sudo apt install jq');
515
- } else if (os.platform() === 'darwin') {
516
- console.log('Install it manually with: brew install jq');
517
- }
518
- }
519
- }
520
-
521
- // Check and install curl
522
- try {
523
- const curlVersion = execSync('curl --version', { encoding: 'utf8' }).split('\n')[0];
524
- success(`curl ${curlVersion.split(' ')[1]}`);
525
- } catch (e) {
526
- warning('curl is not installed. Installing...');
527
- if (installPackage('curl', sudoPassword)) {
528
- success('curl installed successfully');
529
- } else {
530
- warning('Could not install curl automatically');
531
- if (os.platform() === 'linux') {
532
- console.log('Install it manually with: sudo apt install curl');
533
- } else if (os.platform() === 'darwin') {
534
- console.log('Install it manually with: brew install curl');
535
- }
536
- }
537
- }
466
+ // v2.0.0+: jq and curl are no longer needed (pure Node.js implementation)
538
467
 
539
468
  // Check Git
540
469
  try {
@@ -544,23 +473,7 @@ async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false
544
473
  error('Git is not installed. Install Git and try again.');
545
474
  }
546
475
 
547
- // Check standard Unix tools
548
- const unixTools = ['sed', 'awk', 'grep', 'head', 'tail', 'stat', 'tput'];
549
- const missingTools = [];
550
-
551
- unixTools.forEach(tool => {
552
- try {
553
- execSync(`which ${tool}`, { stdio: 'ignore' });
554
- } catch (e) {
555
- missingTools.push(tool);
556
- }
557
- });
558
-
559
- if (missingTools.length === 0) {
560
- success('Standard Unix tools verified');
561
- } else {
562
- error(`Missing standard Unix tools: ${missingTools.join(', ')}. Retry installation in an Ubuntu console`);
563
- }
476
+ // v2.0.0+: Unix tools (sed, awk, grep, etc.) no longer needed (pure Node.js implementation)
564
477
 
565
478
  // Check and install Claude CLI
566
479
  await checkAndInstallClaude();
@@ -576,21 +489,25 @@ async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false
576
489
  sudoPassword = null;
577
490
  }
578
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
+
579
504
  // Check if we need to install dependencies
580
505
  async function checkIfInstallationNeeded() {
581
- const dependencies = ['jq', 'curl'];
582
-
583
- for (const dep of dependencies) {
584
- try {
585
- execSync(`which ${dep}`, { stdio: 'ignore' });
586
- } catch (e) {
587
- return true; // Needs installation
588
- }
589
- }
506
+ // v2.0.0+: Only check Claude CLI (jq and curl no longer needed)
507
+ const claudeCmd = getClaudeCommand();
590
508
 
591
- // Verificar Claude CLI
592
509
  try {
593
- execSync('claude --version', { stdio: 'ignore' });
510
+ execSync(`${claudeCmd} --version`, { stdio: 'ignore' });
594
511
  } catch (e) {
595
512
  return true; // Needs Claude installation
596
513
  }
@@ -598,19 +515,28 @@ async function checkIfInstallationNeeded() {
598
515
  return false;
599
516
  }
600
517
 
601
- // Check and install Claude CLI
518
+ // Check Claude CLI availability
602
519
  async function checkAndInstallClaude() {
520
+ const claudeCmd = getClaudeCommand();
521
+ const platform = isWindows() ? 'Windows (via WSL)' : os.platform();
522
+
603
523
  try {
604
- execSync('claude --version', { stdio: 'ignore' });
605
- success('Claude CLI detected');
524
+ execSync(`${claudeCmd} --version`, { stdio: 'ignore' });
525
+ success(`Claude CLI detected (${platform})`);
606
526
  } catch (e) {
607
- info('Claude CLI not detected. Installing...');
608
- try {
609
- execSync('npm install -g @anthropic-ai/claude-cli', { stdio: 'inherit' });
610
- success('Claude CLI installed successfully');
611
- } catch (installError) {
612
- error('Error installing Claude CLI. Install manually: npm install -g @anthropic-ai/claude-cli');
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');
613
536
  }
537
+
538
+ console.log('\nAfter installation, run: claude-hooks install --force');
539
+ process.exit(1);
614
540
  }
615
541
  }
616
542
 
@@ -618,9 +544,15 @@ async function checkAndInstallClaude() {
618
544
  async function checkClaudeAuth() {
619
545
  info('Checking Claude authentication...');
620
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
+
621
553
  // Use spawn to not block, but with stdio: 'ignore' like the original
622
554
  const authPromise = new Promise((resolve, reject) => {
623
- const child = spawn('claude', ['auth', 'status'], {
555
+ const child = spawn(command, args, {
624
556
  stdio: 'ignore', // Igual que el original
625
557
  detached: false,
626
558
  windowsHide: true
@@ -736,20 +668,20 @@ function configureGit() {
736
668
  info('Configuring Git...');
737
669
 
738
670
  try {
739
- // Configure line endings for WSL
740
- execSync('git config core.autocrlf input', { stdio: 'ignore' });
741
- success('Line endings configured for WSL (core.autocrlf = input)');
742
-
743
- // Try to configure on Windows through PowerShell
744
- try {
745
- 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' });
746
676
  success('Line endings configured for Windows (core.autocrlf = true)');
747
- } catch (psError) {
748
- info('Could not configure automatically on Windows');
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)');
749
681
  }
750
682
 
751
683
  } catch (e) {
752
- warning('Error configuring Git');
684
+ warning('Error configuring Git: ' + e.message);
753
685
  }
754
686
  }
755
687
 
@@ -822,7 +754,7 @@ function disable(hookName) {
822
754
  }
823
755
 
824
756
  // Analyze-diff command
825
- function analyzeDiff(args) {
757
+ async function analyzeDiff(args) {
826
758
  if (!checkGitRepo()) {
827
759
  error('You are not in a Git repository.');
828
760
  return;
@@ -930,10 +862,6 @@ function analyzeDiff(args) {
930
862
  return;
931
863
  }
932
864
 
933
- // Create the prompt for Claude
934
- const tempDir = `/tmp/claude-analyze-${Date.now()}`;
935
- fs.mkdirSync(tempDir, { recursive: true });
936
-
937
865
  // Check if subagents should be used
938
866
  const useSubagents = process.env.CLAUDE_USE_SUBAGENTS === 'true';
939
867
  const subagentModel = process.env.CLAUDE_SUBAGENT_MODEL || 'haiku';
@@ -946,14 +874,13 @@ function analyzeDiff(args) {
946
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`
947
875
  : '';
948
876
 
949
- const promptFile = path.join(tempDir, 'prompt.txt');
950
877
  const prompt = `Analyze the following changes. CONTEXT: ${contextDescription}
951
878
  ${subagentInstruction}
952
879
  Please generate:
953
880
  1. A concise and descriptive PR title (maximum 72 characters)
954
881
  2. A detailed PR description that includes:
955
882
  - Summary of changes
956
- - Motivation/context
883
+ - Motivation/context
957
884
  - Type of change (feature/fix/refactor/docs/etc)
958
885
  - Recommended testing
959
886
  3. A suggested branch name following the format: type/short-description (example: feature/add-user-auth, fix/memory-leak)
@@ -979,30 +906,15 @@ ${diffFiles}
979
906
  === FULL DIFF ===
980
907
  ${fullDiff.substring(0, 50000)} ${fullDiff.length > 50000 ? '\n... (truncated diff)' : ''}`;
981
908
 
982
- fs.writeFileSync(promptFile, prompt);
983
-
984
909
  info('Sending to Claude for analysis...');
985
910
  const startTime = Date.now();
986
911
 
987
912
  try {
988
- const response = execSync(`claude < "${promptFile}"`, { encoding: 'utf8', maxBuffer: 1024 * 1024 * 10 });
989
-
990
- // Extraer el JSON de la respuesta
991
- const jsonMatch = response.match(/\{[\s\S]*\}/);
992
- if (!jsonMatch) {
993
- error('Did not receive a valid JSON response from Claude.');
994
- console.log('Complete response:', response);
995
- return;
996
- }
913
+ // Use cross-platform executeClaude from claude-client.js
914
+ const response = await executeClaude(prompt, { timeout: 180000 }); // 3 minutes for diff analysis
997
915
 
998
- let result;
999
- try {
1000
- result = JSON.parse(jsonMatch[0]);
1001
- } catch (e) {
1002
- error('Error parsing JSON response: ' + e.message);
1003
- console.log('JSON received:', jsonMatch[0]);
1004
- return;
1005
- }
916
+ // Extract JSON from response using claude-client utility
917
+ const result = extractJSON(response);
1006
918
 
1007
919
  // Show the results
1008
920
  console.log('');
@@ -1080,9 +992,6 @@ ${fullDiff.substring(0, 50000)} ${fullDiff.length > 50000 ? '\n... (truncated di
1080
992
 
1081
993
  } catch (e) {
1082
994
  error('Error executing Claude: ' + e.message);
1083
- } finally {
1084
- // Clean temporary files
1085
- fs.rmSync(tempDir, { recursive: true, force: true });
1086
995
  }
1087
996
  }
1088
997
 
@@ -1192,7 +1101,7 @@ async function update() {
1192
1101
  info('Checking latest available version...');
1193
1102
 
1194
1103
  try {
1195
- const currentVersion = require('../package.json').version;
1104
+ const currentVersion = getPackageJson().version;
1196
1105
  const latestVersion = await getLatestVersion('claude-git-hooks');
1197
1106
 
1198
1107
  const comparison = compareVersions(currentVersion, latestVersion);
@@ -1327,7 +1236,7 @@ async function main() {
1327
1236
  status();
1328
1237
  break;
1329
1238
  case 'analyze-diff':
1330
- analyzeDiff(args.slice(1));
1239
+ await analyzeDiff(args.slice(1));
1331
1240
  break;
1332
1241
  case 'help':
1333
1242
  case '--help':