claude-git-hooks 2.17.0 → 2.17.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 CHANGED
@@ -5,23 +5,44 @@ Todos los cambios notables en este proyecto se documentarán en este archivo.
5
5
  El formato está basado en [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  y este proyecto adhiere a [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.17.2] - 2026-03-02
9
+
10
+ ### 🐛 Fixed
11
+ - Improved pom.xml version detection to correctly distinguish project version from dependency/property versions (#76)
12
+ - Fixed false version mismatch errors for Maven submodules with inherited parent versions
13
+
14
+
15
+ ## [2.17.1] - 2026-03-02
16
+
17
+ ### 🐛 Fixed
18
+
19
+ - Installer no longer crashes with `ENOTDIR` when running inside a Git worktree where `.git` is a file pointer instead of a directory
20
+ - Hook management commands (`enable`, `disable`, `status`, `uninstall`) now correctly resolve the hooks path in worktree repos
21
+
22
+ ### 🔧 Changed
23
+
24
+ - Extracted `getGitHooksPath()` to `helpers.js` — uses `git rev-parse --git-common-dir` to find the shared hooks directory in both regular repos and worktrees
25
+
8
26
  ## [2.17.0] - 2026-02-16
9
27
 
10
28
  ### ✨ Added
29
+
11
30
  - Automatic GitHub token setup now runs after installation
12
31
 
13
32
  ### 🔧 Changed
33
+
14
34
  - Simplified CLI help output to show concise command reference instead of verbose documentation
15
35
  - Commands in help output are now listed in alphabetical order for easier navigation
16
36
  - Installation success message now shows single help reference instead of detailed usage examples
17
37
 
18
38
  ### 🗑️ Removed
19
- - Removed extensive inline documentation from help output (use README for detailed docs)
20
39
 
40
+ - Removed extensive inline documentation from help output (use README for detailed docs)
21
41
 
22
42
  ## [2.16.0] - 2026-02-15
23
43
 
24
44
  ### ✨ Added
45
+
25
46
  - Multi-file version discovery with recursive scanning up to 3 levels deep
26
47
  - Per-file version editing with `--interactive` flag for advanced control
27
48
  - Support for additional project types: build.gradle, build.gradle.kts, pyproject.toml, Cargo.toml, version.sbt
@@ -32,22 +53,25 @@ y este proyecto adhiere a [Semantic Versioning](https://semver.org/spec/v2.0.0.h
32
53
  - New UI components: `promptToggleList()` and `promptEditField()` in interactive-ui.js
33
54
 
34
55
  ### 🔧 Changed
56
+
35
57
  - Refactored version-manager.js to use new discovery-based API with `discoverVersionFiles()`, `updateVersionFiles()`, and `modifySuffix()`
36
58
  - Bump-version workflow now supports selecting specific files to update
37
59
  - Each file can have an independent target version when using per-file editing mode
38
60
  - Updated CLAUDE.md documentation with new version bump flow and API changes
39
61
 
40
62
  ### ⚠️ Deprecated
41
- - Legacy version-manager functions `detectProjectType()`, `getCurrentVersion()`, `updateVersion()`, and `getDiscoveredPaths()` replaced by new discovery API
42
63
 
64
+ - Legacy version-manager functions `detectProjectType()`, `getCurrentVersion()`, `updateVersion()`, and `getDiscoveredPaths()` replaced by new discovery API
43
65
 
44
66
  ## [2.15.5] - 2026-02-12
45
67
 
46
68
  ### ✨ Added
69
+
47
70
  - Added comprehensive CI-safe integration test suite that runs without Claude CLI dependency
48
71
  - Added new test scripts: `test:integration:ci` for CI environments and `test:changed` for testing only modified files
49
72
 
50
73
  ### 🔧 Changed
74
+
51
75
  - Optimized CI matrix to run full OS/Node.js combinations only on push events, reducing PR build times
52
76
  - Refactored integration tests to verify installation workflow, hook lifecycle, config persistence, and CLI routing
53
77
  - Improved timeout handling in Claude CLI client by clearing timeouts consistently on all exit paths
@@ -55,35 +79,37 @@ y este proyecto adhiere a [Semantic Versioning](https://semver.org/spec/v2.0.0.h
55
79
  - Reduced Jest verbosity and coverage reporters for cleaner CI output
56
80
 
57
81
  ### 🐛 Fixed
82
+
58
83
  - Fixed potential timeout leak in claude-client.js where timeouts were not cleared on error or stdin failures
59
84
 
60
85
  ### 🗑️ Removed
61
- - Removed unused variables and parameters across multiple modules (analyze-diff, create-pr, install, config, changelog-generator, claude-client, claude-diagnostics, file-utils, git-operations, prompt-builder)
62
86
 
87
+ - Removed unused variables and parameters across multiple modules (analyze-diff, create-pr, install, config, changelog-generator, claude-client, claude-diagnostics, file-utils, git-operations, prompt-builder)
63
88
 
64
89
  ## [2.15.4] - 2026-02-11
65
90
 
66
91
  ### ✨ Added
92
+
67
93
  - Added comprehensive unit tests for claude-diagnostics.js covering error detection, formatting, and recovery classification
68
94
  - Added unit tests for file-utils.js covering directory creation and file writing operations
69
95
 
70
-
71
96
  ## [2.15.3] - 2026-02-10
72
97
 
73
98
  ### ✨ Added
99
+
74
100
  - Unit tests for `generate-changelog.js` command covering CHANGELOG generation, version detection, and error handling
75
101
  - Unit tests for `github-client.js` covering CODEOWNERS parsing, reviewer detection, and GitHub repo parsing
76
102
  - Unit tests for `hooks.js` command covering enable, disable, status, and uninstall operations
77
103
 
78
-
79
104
  ## [2.15.2] - 2026-02-10
80
105
 
81
106
  ### 🔧 Changed
107
+
82
108
  - The bump-version command now uses the configured default branch (from github.pr.defaultBase) in the 'Next steps' output instead of hardcoded 'main'
83
109
 
84
110
  ### 🐛 Fixed
85
- - Fixed cross-platform path assertions in unit tests to use partial matching instead of exact paths, improving Windows compatibility
86
111
 
112
+ - Fixed cross-platform path assertions in unit tests to use partial matching instead of exact paths, improving Windows compatibility
87
113
 
88
114
  ## [2.15.1] - 2026-02-10
89
115
 
@@ -95,6 +95,26 @@ export function getTemplatesPath() {
95
95
  return path.join(__dirname, '..', '..', 'templates');
96
96
  }
97
97
 
98
+ /**
99
+ * Get the git hooks directory, correctly handling worktrees.
100
+ * In a worktree, hooks live in the main repo's .git/hooks, not the worktree's .git file.
101
+ * Uses `git rev-parse --git-common-dir` to find the shared git directory.
102
+ * @returns {string} Path to the git hooks directory
103
+ */
104
+ export function getGitHooksPath() {
105
+ try {
106
+ let gitCommonDir = execSync('git rev-parse --git-common-dir', { encoding: 'utf8' }).trim();
107
+ // Handle Windows paths when running under WSL (e.g. C:\... -> /mnt/c/...)
108
+ if (/^[A-Za-z]:/.test(gitCommonDir)) {
109
+ gitCommonDir = gitCommonDir.replace(/^([A-Za-z]):/, (_, drive) => `/mnt/${drive.toLowerCase()}`);
110
+ gitCommonDir = gitCommonDir.replace(/\\/g, '/');
111
+ }
112
+ return path.join(gitCommonDir, 'hooks');
113
+ } catch (e) {
114
+ return path.join('.git', 'hooks');
115
+ }
116
+ }
117
+
98
118
  /**
99
119
  * Detect if running on Windows
100
120
  * Why: Need to use 'wsl claude' instead of 'claude' on Windows
@@ -10,7 +10,8 @@ import {
10
10
  success,
11
11
  info,
12
12
  warning,
13
- checkGitRepo
13
+ checkGitRepo,
14
+ getGitHooksPath
14
15
  } from './helpers.js';
15
16
 
16
17
  /**
@@ -22,11 +23,12 @@ export function runEnable(hookName) {
22
23
  error('You are not in a Git repository.');
23
24
  }
24
25
 
26
+ const hooksDir = getGitHooksPath();
25
27
  const hooks = hookName ? [hookName] : ['pre-commit', 'prepare-commit-msg'];
26
28
 
27
29
  hooks.forEach(hook => {
28
- const disabledPath = `.git/hooks/${hook}.disabled`;
29
- const enabledPath = `.git/hooks/${hook}`;
30
+ const disabledPath = path.join(hooksDir, `${hook}.disabled`);
31
+ const enabledPath = path.join(hooksDir, hook);
30
32
 
31
33
  if (fs.existsSync(disabledPath)) {
32
34
  fs.renameSync(disabledPath, enabledPath);
@@ -48,11 +50,12 @@ export function runDisable(hookName) {
48
50
  error('You are not in a Git repository.');
49
51
  }
50
52
 
53
+ const hooksDir = getGitHooksPath();
51
54
  const hooks = hookName ? [hookName] : ['pre-commit', 'prepare-commit-msg'];
52
55
 
53
56
  hooks.forEach(hook => {
54
- const enabledPath = `.git/hooks/${hook}`;
55
- const disabledPath = `.git/hooks/${hook}.disabled`;
57
+ const enabledPath = path.join(hooksDir, hook);
58
+ const disabledPath = path.join(hooksDir, `${hook}.disabled`);
56
59
 
57
60
  if (fs.existsSync(enabledPath)) {
58
61
  fs.renameSync(enabledPath, disabledPath);
@@ -75,10 +78,11 @@ export function runStatus() {
75
78
 
76
79
  info('Claude Git Hooks status:\n');
77
80
 
81
+ const hooksDir = getGitHooksPath();
78
82
  const hooks = ['pre-commit', 'prepare-commit-msg'];
79
83
  hooks.forEach(hook => {
80
- const enabledPath = `.git/hooks/${hook}`;
81
- const disabledPath = `.git/hooks/${hook}.disabled`;
84
+ const enabledPath = path.join(hooksDir, hook);
85
+ const disabledPath = path.join(hooksDir, `${hook}.disabled`);
82
86
 
83
87
  if (fs.existsSync(enabledPath)) {
84
88
  success(`${hook}: enabled`);
@@ -135,7 +139,7 @@ export function runUninstall() {
135
139
 
136
140
  info('Uninstalling Claude Git Hooks...');
137
141
 
138
- const hooksPath = '.git/hooks';
142
+ const hooksPath = getGitHooksPath();
139
143
  const hooks = ['pre-commit', 'prepare-commit-msg'];
140
144
 
141
145
  hooks.forEach(hook => {
@@ -23,6 +23,7 @@ import {
23
23
  warning,
24
24
  checkGitRepo,
25
25
  getTemplatesPath,
26
+ getGitHooksPath,
26
27
  isWindows,
27
28
  getClaudeCommand,
28
29
  getPackageJson,
@@ -402,7 +403,7 @@ export async function runInstall(args) {
402
403
  await checkAndInstallDependencies(skipAuth);
403
404
 
404
405
  const templatesPath = getTemplatesPath();
405
- const hooksPath = '.git/hooks';
406
+ const hooksPath = getGitHooksPath();
406
407
 
407
408
  // Create hooks directory if it doesn't exist
408
409
  if (!fs.existsSync(hooksPath)) {
@@ -22,6 +22,7 @@
22
22
  import fs from 'fs';
23
23
  import path from 'path';
24
24
  import { getRepoRoot } from './git-operations.js';
25
+ import { getGitHooksPath } from '../commands/helpers.js';
25
26
 
26
27
  /**
27
28
  * Gets installation diagnostics
@@ -60,7 +61,7 @@ export const getInstallationDiagnostics = () => {
60
61
  diagnostics.presetsDirPath = path.join(diagnostics.claudeDirPath, 'presets');
61
62
  diagnostics.presetsDirExists = fs.existsSync(diagnostics.presetsDirPath);
62
63
 
63
- const gitHooksPath = path.join(diagnostics.repoRoot, '.git', 'hooks');
64
+ const gitHooksPath = getGitHooksPath();
64
65
  diagnostics.gitHooksExists = fs.existsSync(gitHooksPath);
65
66
  } catch (error) {
66
67
  // Not in a git repository - diagnostics.repoRoot will be null
@@ -21,6 +21,64 @@ import logger from './logger.js';
21
21
  */
22
22
  let cachedDiscoveryResult = null;
23
23
 
24
+ /**
25
+ * Extracts the project version from pom.xml content (not dependency/property versions)
26
+ * Why: Maven POMs require context-aware parsing to distinguish the project's own
27
+ * <version> tag from dependency versions, property references (${...}), or
28
+ * inherited versions inside <parent> blocks.
29
+ *
30
+ * Taxative rule: In a Maven POM, the project coordinates (<groupId>, <artifactId>,
31
+ * <version>, <packaging>) are direct children of <project> and must appear BEFORE
32
+ * any section-level element. After </parent>, the project's own <version> (if explicit)
33
+ * will be in this "header" region before any section element.
34
+ *
35
+ * @param {string} content - pom.xml file content
36
+ * @returns {{ version: string|null, inherited: boolean }}
37
+ * - version: The effective project version (explicit or from parent)
38
+ * - inherited: true if version comes from <parent> block (no explicit project <version>)
39
+ */
40
+ function extractPomProjectVersion(content) {
41
+ const hasParent = /<parent>[\s\S]*?<\/parent>/.test(content);
42
+
43
+ if (hasParent) {
44
+ // Section-level elements that mark the end of the project coordinates region.
45
+ // Any <version> tag inside these sections belongs to dependencies, plugins, etc.
46
+ const sectionTags = [
47
+ 'properties', 'dependencies', 'dependencyManagement',
48
+ 'build', 'profiles', 'modules', 'reporting',
49
+ 'distributionManagement', 'repositories', 'pluginRepositories',
50
+ '\\/project'
51
+ ].join('|');
52
+
53
+ // Extract the "header" region: between </parent> and the first section element
54
+ const headerRegex = new RegExp(
55
+ `<\\/parent>([\\s\\S]*?)(?=<(?:${sectionTags})\\b)`
56
+ );
57
+ const headerMatch = content.match(headerRegex);
58
+
59
+ if (headerMatch) {
60
+ const versionMatch = headerMatch[1].match(/<version>([^<]+)<\/version>/);
61
+ if (versionMatch) {
62
+ return { version: versionMatch[1].trim(), inherited: false };
63
+ }
64
+ }
65
+
66
+ // No explicit project <version> found — fall back to <parent><version>
67
+ const parentVersionRegex = /<parent>[\s\S]*?<version>([^<]+)<\/version>[\s\S]*?<\/parent>/;
68
+ const parentMatch = content.match(parentVersionRegex);
69
+ if (parentMatch) {
70
+ return { version: parentMatch[1].trim(), inherited: true };
71
+ }
72
+
73
+ return { version: null, inherited: false };
74
+ }
75
+
76
+ // No parent block — first <version> after <project> is the project version
77
+ const projectVersionRegex = /<project[^>]*>[\s\S]*?<version>([^<]+)<\/version>/;
78
+ const match = content.match(projectVersionRegex);
79
+ return { version: match ? match[1].trim() : null, inherited: false };
80
+ }
81
+
24
82
  /**
25
83
  * Registry of supported version file types
26
84
  * Why: Extensible system for multi-language version management
@@ -61,17 +119,8 @@ const VERSION_FILE_TYPES = {
61
119
  readVersion: (filePath) => {
62
120
  try {
63
121
  const content = fs.readFileSync(filePath, 'utf8');
64
- const hasParent = /<parent>[\s\S]*?<\/parent>/.test(content);
65
-
66
- if (hasParent) {
67
- const afterParentRegex = /<\/parent>[\s\S]*?<version>([^<]+)<\/version>/;
68
- const match = content.match(afterParentRegex);
69
- return match ? match[1].trim() : null;
70
- } else {
71
- const projectVersionRegex = /<project[^>]*>[\s\S]*?<version>([^<]+)<\/version>/;
72
- const match = content.match(projectVersionRegex);
73
- return match ? match[1].trim() : null;
74
- }
122
+ const { version } = extractPomProjectVersion(content);
123
+ return version;
75
124
  } catch (error) {
76
125
  logger.debug('version-manager - VERSION_FILE_TYPES', 'Failed to read pom.xml', {
77
126
  filePath,
@@ -82,15 +131,22 @@ const VERSION_FILE_TYPES = {
82
131
  },
83
132
  writeVersion: (filePath, newVersion) => {
84
133
  const content = fs.readFileSync(filePath, 'utf8');
85
- const hasParent = /<parent>[\s\S]*?<\/parent>/.test(content);
134
+ const { inherited } = extractPomProjectVersion(content);
86
135
 
87
136
  let newContent;
88
- if (hasParent) {
89
- const afterParentRegex = /(<\/parent>[\s\S]*?<version>)[^<]+(<\/version>)/;
90
- newContent = content.replace(afterParentRegex, `$1${newVersion}$2`);
137
+ if (inherited) {
138
+ // Version is inherited from parent — update <parent><version> tag
139
+ const parentRegex = /(<parent>[\s\S]*?<version>)[^<]+(<\/version>[\s\S]*?<\/parent>)/;
140
+ newContent = content.replace(parentRegex, `$1${newVersion}$2`);
91
141
  } else {
92
- const projectVersionRegex = /(<project[^>]*>[\s\S]*?<version>)[^<]+(<\/version>)/;
93
- newContent = content.replace(projectVersionRegex, `$1${newVersion}$2`);
142
+ const hasParent = /<parent>[\s\S]*?<\/parent>/.test(content);
143
+ if (hasParent) {
144
+ const afterParentRegex = /(<\/parent>[\s\S]*?<version>)[^<]+(<\/version>)/;
145
+ newContent = content.replace(afterParentRegex, `$1${newVersion}$2`);
146
+ } else {
147
+ const projectVersionRegex = /(<project[^>]*>[\s\S]*?<version>)[^<]+(<\/version>)/;
148
+ newContent = content.replace(projectVersionRegex, `$1${newVersion}$2`);
149
+ }
94
150
  }
95
151
 
96
152
  if (content === newContent) {
package/package.json CHANGED
@@ -1,68 +1,68 @@
1
- {
2
- "name": "claude-git-hooks",
3
- "version": "2.17.0",
4
- "description": "Git hooks with Claude CLI for code analysis and automatic commit messages",
5
- "type": "module",
6
- "bin": {
7
- "claude-hooks": "./bin/claude-hooks"
8
- },
9
- "scripts": {
10
- "test": "npm run test:all",
11
- "test:all": "npm run lint && npm run test:smoke && npm run test:unit && npm run test:integration",
12
- "test:smoke": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/smoke --maxWorkers=1",
13
- "test:unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --forceExit",
14
- "test:integration": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration --maxWorkers=1 --testTimeout=30000 --forceExit",
15
- "test:integration:ci": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration/ci-safe.test.js --maxWorkers=1 --testTimeout=30000 --forceExit",
16
- "test:changed": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --changedSince=main --forceExit",
17
- "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
18
- "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
19
- "lint": "eslint lib/ bin/claude-hooks",
20
- "lint:fix": "eslint lib/ bin/claude-hooks --fix",
21
- "format": "prettier --write \"lib/**/*.js\" \"bin/**\" \"test/**/*.js\"",
22
- "precommit": "npm run lint && npm run test:smoke",
23
- "prepublishOnly": "npm run test:all"
24
- },
25
- "keywords": [
26
- "git",
27
- "hooks",
28
- "claude",
29
- "ai",
30
- "code-review",
31
- "commit-messages",
32
- "pre-commit",
33
- "automation"
34
- ],
35
- "author": "Pablo Rovito",
36
- "license": "MIT",
37
- "repository": {
38
- "type": "git",
39
- "url": "https://github.com/pablorovito/claude-git-hooks.git"
40
- },
41
- "engines": {
42
- "node": ">=16.9.0"
43
- },
44
- "engineStrict": false,
45
- "os": [
46
- "darwin",
47
- "linux",
48
- "win32"
49
- ],
50
- "preferGlobal": true,
51
- "files": [
52
- "bin/",
53
- "lib/",
54
- "templates/",
55
- "README.md",
56
- "CHANGELOG.md",
57
- "LICENSE"
58
- ],
59
- "dependencies": {
60
- "@octokit/rest": "^21.0.0"
61
- },
62
- "devDependencies": {
63
- "@types/jest": "^29.5.0",
64
- "eslint": "^8.57.0",
65
- "jest": "^29.7.0",
66
- "prettier": "^3.2.0"
67
- }
68
- }
1
+ {
2
+ "name": "claude-git-hooks",
3
+ "version": "2.17.2",
4
+ "description": "Git hooks with Claude CLI for code analysis and automatic commit messages",
5
+ "type": "module",
6
+ "bin": {
7
+ "claude-hooks": "./bin/claude-hooks"
8
+ },
9
+ "scripts": {
10
+ "test": "npm run test:all",
11
+ "test:all": "npm run lint && npm run test:smoke && npm run test:unit && npm run test:integration",
12
+ "test:smoke": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/smoke --maxWorkers=1",
13
+ "test:unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --forceExit",
14
+ "test:integration": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration --maxWorkers=1 --testTimeout=30000 --forceExit",
15
+ "test:integration:ci": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration/ci-safe.test.js --maxWorkers=1 --testTimeout=30000 --forceExit",
16
+ "test:changed": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --changedSince=main --forceExit",
17
+ "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
18
+ "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
19
+ "lint": "eslint lib/ bin/claude-hooks",
20
+ "lint:fix": "eslint lib/ bin/claude-hooks --fix",
21
+ "format": "prettier --write \"lib/**/*.js\" \"bin/**\" \"test/**/*.js\"",
22
+ "precommit": "npm run lint && npm run test:smoke",
23
+ "prepublishOnly": "npm run test:all"
24
+ },
25
+ "keywords": [
26
+ "git",
27
+ "hooks",
28
+ "claude",
29
+ "ai",
30
+ "code-review",
31
+ "commit-messages",
32
+ "pre-commit",
33
+ "automation"
34
+ ],
35
+ "author": "Pablo Rovito",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/pablorovito/claude-git-hooks.git"
40
+ },
41
+ "engines": {
42
+ "node": ">=16.9.0"
43
+ },
44
+ "engineStrict": false,
45
+ "os": [
46
+ "darwin",
47
+ "linux",
48
+ "win32"
49
+ ],
50
+ "preferGlobal": true,
51
+ "files": [
52
+ "bin/",
53
+ "lib/",
54
+ "templates/",
55
+ "README.md",
56
+ "CHANGELOG.md",
57
+ "LICENSE"
58
+ ],
59
+ "dependencies": {
60
+ "@octokit/rest": "^21.0.0"
61
+ },
62
+ "devDependencies": {
63
+ "@types/jest": "^29.5.0",
64
+ "eslint": "^8.57.0",
65
+ "jest": "^29.7.0",
66
+ "prettier": "^3.2.0"
67
+ }
68
+ }