claude-git-hooks 2.12.0 → 2.14.1

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.
@@ -35,60 +35,65 @@ class ResolutionPromptError extends Error {
35
35
  }
36
36
 
37
37
  /**
38
- * Formats blocking issues for resolution prompt
38
+ * Formats issues for resolution prompt
39
39
  * Why: Structures issues in readable markdown format for AI processing
40
40
  *
41
- * @param {Array<Object>} blockingIssues - Array of blocking issues
41
+ * @param {Array<Object>} issues - Array of issues (blocking or detailed)
42
42
  * @returns {string} Formatted markdown
43
43
  *
44
- * Issue object structure:
45
- * {
46
- * description: string, // Human-readable description
47
- * file: string, // Relative file path
48
- * line: number, // Line number
49
- * method: string, // Method or class name
50
- * severity: string // 'blocker' | 'critical'
51
- * }
44
+ * Supports both blocking issue format and detailed issue format:
45
+ * Blocking: { description, file, line, method, severity }
46
+ * Detailed: { message, file, line, method, severity, type, rule }
52
47
  */
53
- const formatBlockingIssues = (blockingIssues) => {
48
+ const formatBlockingIssues = (issues) => {
54
49
  logger.debug(
55
50
  'resolution-prompt - formatBlockingIssues',
56
- 'Formatting blocking issues',
57
- { issueCount: blockingIssues.length }
51
+ 'Formatting issues',
52
+ { issueCount: issues?.length || 0 }
58
53
  );
59
54
 
60
- if (!Array.isArray(blockingIssues) || blockingIssues.length === 0) {
61
- return 'No blocking issues found.';
55
+ if (!Array.isArray(issues) || issues.length === 0) {
56
+ return 'No issues found.';
62
57
  }
63
58
 
64
- return blockingIssues
59
+ return issues
65
60
  .map((issue, index) => {
66
61
  const issueNum = index + 1;
67
62
  const severity = (issue.severity || 'unknown').toUpperCase();
63
+ // Support both 'description' (blocking) and 'message' (detailed) formats
64
+ const description = issue.description || issue.message || 'No description';
65
+ const type = issue.type ? ` - ${issue.type}` : '';
68
66
 
69
- return `### Issue #${issueNum} [${severity}]
70
- **Description:** ${issue.description || 'No description'}
71
- **Location:** ${issue.file || 'unknown'}:${issue.line || '?'}
72
- **Method/Class:** ${issue.method || 'unknown'}
73
- `;
67
+ let formatted = `### Issue #${issueNum} [${severity}]${type}
68
+ **Description:** ${description}
69
+ **Location:** ${issue.file || 'unknown'}:${issue.line || '?'}`;
70
+
71
+ if (issue.method) {
72
+ formatted += `\n**Method/Class:** ${issue.method}`;
73
+ }
74
+ if (issue.rule) {
75
+ formatted += `\n**Rule:** ${issue.rule}`;
76
+ }
77
+
78
+ return `${formatted}\n`;
74
79
  })
75
80
  .join('\n');
76
81
  };
77
82
 
78
83
  /**
79
- * Gets unique file paths from blocking issues
84
+ * Gets unique file paths from issues
80
85
  * Why: Need to include full content of affected files
81
86
  *
82
- * @param {Array<Object>} blockingIssues - Array of blocking issues
87
+ * @param {Array<Object>} issues - Array of issues (blocking or detailed)
83
88
  * @returns {Array<string>} Unique file paths
84
89
  */
85
- const getAffectedFiles = (blockingIssues) => {
86
- if (!Array.isArray(blockingIssues)) {
90
+ const getAffectedFiles = (issues) => {
91
+ if (!Array.isArray(issues)) {
87
92
  return [];
88
93
  }
89
94
 
90
95
  // Extract unique file paths
91
- const filePaths = blockingIssues
96
+ const filePaths = issues
92
97
  .map(issue => issue.file)
93
98
  .filter(file => file && typeof file === 'string');
94
99
 
@@ -97,7 +102,7 @@ const getAffectedFiles = (blockingIssues) => {
97
102
  logger.debug(
98
103
  'resolution-prompt - getAffectedFiles',
99
104
  'Extracted affected files',
100
- { totalIssues: blockingIssues.length, uniqueFiles: uniqueFiles.length }
105
+ { totalIssues: issues.length, uniqueFiles: uniqueFiles.length }
101
106
  );
102
107
 
103
108
  return uniqueFiles;
@@ -195,6 +200,12 @@ const generateResolutionPrompt = async (
195
200
  const absoluteOutputPath = path.join(repoRoot, finalOutputPath);
196
201
  const absoluteTemplatePath = path.join(repoRoot, templatePath);
197
202
 
203
+ // Use details array (all issues) if available, fallback to blockingIssues
204
+ // Why: analyze command provides full details, pre-commit may only have blockingIssues
205
+ const allIssues = (Array.isArray(analysisResult.details) && analysisResult.details.length > 0)
206
+ ? analysisResult.details
207
+ : (analysisResult.blockingIssues || []);
208
+
198
209
  logger.debug(
199
210
  'resolution-prompt - generateResolutionPrompt',
200
211
  'Generating resolution prompt',
@@ -202,7 +213,8 @@ const generateResolutionPrompt = async (
202
213
  repoRoot,
203
214
  outputPath: absoluteOutputPath,
204
215
  templatePath: absoluteTemplatePath,
205
- blockingIssuesCount: analysisResult.blockingIssues?.length || 0
216
+ issueCount: allIssues.length,
217
+ usingDetails: Array.isArray(analysisResult.details) && analysisResult.details.length > 0
206
218
  }
207
219
  );
208
220
 
@@ -215,11 +227,11 @@ const generateResolutionPrompt = async (
215
227
  const branchName = getCurrentBranch();
216
228
  const commitSha = 'pending';
217
229
 
218
- // Format blocking issues
219
- const issuesFormatted = formatBlockingIssues(analysisResult.blockingIssues || []);
230
+ // Format all issues
231
+ const issuesFormatted = formatBlockingIssues(allIssues);
220
232
 
221
233
  // Get and format affected files
222
- const affectedFiles = getAffectedFiles(analysisResult.blockingIssues || []);
234
+ const affectedFiles = getAffectedFiles(allIssues);
223
235
  const fileContentsFormatted = await formatFileContents(affectedFiles);
224
236
 
225
237
  // Replace placeholders
@@ -232,6 +244,13 @@ const generateResolutionPrompt = async (
232
244
  .replace(/{{BLOCKING_ISSUES}}/g, issuesFormatted)
233
245
  .replace(/{{FILE_CONTENTS}}/g, fileContentsFormatted);
234
246
 
247
+ // Prepend false positive check instructions
248
+ const falsePositiveCheck = `FIRST STEP: Analyze each issue below. Classify into FALSE POSITIVE or TRUE ISSUE. Fix TRUE ISSUES only.
249
+
250
+ `;
251
+
252
+ prompt = falsePositiveCheck + prompt;
253
+
235
254
  // Ensure output directory exists
236
255
  const outputDir = path.dirname(absoluteOutputPath);
237
256
  await fs.mkdir(outputDir, { recursive: true });
@@ -270,7 +289,7 @@ const generateResolutionPrompt = async (
270
289
 
271
290
  /**
272
291
  * Checks if resolution prompt should be generated
273
- * Why: Only generate when there are actual blocking issues
292
+ * Why: Generate when there are any issues (blocking or non-blocking)
274
293
  *
275
294
  * @param {Object} analysisResult - Analysis result
276
295
  * @returns {boolean} True if should generate prompt
@@ -278,17 +297,21 @@ const generateResolutionPrompt = async (
278
297
  const shouldGeneratePrompt = (analysisResult) => {
279
298
  const hasBlockingIssues = Array.isArray(analysisResult.blockingIssues) &&
280
299
  analysisResult.blockingIssues.length > 0;
300
+ const hasDetailedIssues = Array.isArray(analysisResult.details) &&
301
+ analysisResult.details.length > 0;
281
302
 
282
303
  logger.debug(
283
304
  'resolution-prompt - shouldGeneratePrompt',
284
305
  'Checking if resolution prompt needed',
285
306
  {
286
307
  hasBlockingIssues,
287
- blockingIssuesCount: analysisResult.blockingIssues?.length || 0
308
+ hasDetailedIssues,
309
+ blockingIssuesCount: analysisResult.blockingIssues?.length || 0,
310
+ detailedIssuesCount: analysisResult.details?.length || 0
288
311
  }
289
312
  );
290
313
 
291
- return hasBlockingIssues;
314
+ return hasBlockingIssues || hasDetailedIssues;
292
315
  };
293
316
 
294
317
  export {
@@ -5,7 +5,7 @@
5
5
  * Handles version operations for:
6
6
  * - package.json (Node.js projects)
7
7
  * - pom.xml (Maven projects)
8
- * - Mixed monorepos (both files)
8
+ * - Mixed monorepos (both files, including in subdirectories)
9
9
  *
10
10
  * Supports semantic versioning with optional suffixes (SNAPSHOT, RC, etc.)
11
11
  */
@@ -25,6 +25,100 @@ const PROJECT_TYPES = {
25
25
  NONE: 'none'
26
26
  };
27
27
 
28
+ /**
29
+ * Cache for discovered version file paths
30
+ * Why: Avoid re-discovering paths on every operation
31
+ */
32
+ let discoveredPaths = {
33
+ packageJson: null,
34
+ pomXml: null
35
+ };
36
+
37
+ /**
38
+ * Discovers version files in repo root and one level deep
39
+ * Why: Support monorepos with version files in subdirectories
40
+ *
41
+ * @returns {Object} Discovered paths: { packageJson, pomXml }
42
+ */
43
+ export function discoverVersionFiles() {
44
+ logger.debug('version-manager - discoverVersionFiles', 'Searching for version files');
45
+
46
+ const repoRoot = getRepoRoot();
47
+ const result = { packageJson: null, pomXml: null };
48
+
49
+ // First check root level
50
+ const rootPackageJson = path.join(repoRoot, 'package.json');
51
+ const rootPomXml = path.join(repoRoot, 'pom.xml');
52
+
53
+ if (fs.existsSync(rootPackageJson)) {
54
+ result.packageJson = rootPackageJson;
55
+ }
56
+ if (fs.existsSync(rootPomXml)) {
57
+ result.pomXml = rootPomXml;
58
+ }
59
+
60
+ // If both found at root, we're done
61
+ if (result.packageJson && result.pomXml) {
62
+ logger.debug('version-manager - discoverVersionFiles', 'Found both at root', result);
63
+ return result;
64
+ }
65
+
66
+ // Search one level deep in subdirectories
67
+ try {
68
+ const entries = fs.readdirSync(repoRoot, { withFileTypes: true });
69
+ const subdirs = entries.filter(e =>
70
+ e.isDirectory() &&
71
+ !e.name.startsWith('.') &&
72
+ !e.name.startsWith('node_modules') &&
73
+ !e.name.startsWith('target') &&
74
+ !e.name.startsWith('build') &&
75
+ !e.name.startsWith('dist')
76
+ );
77
+
78
+ for (const subdir of subdirs) {
79
+ const subdirPath = path.join(repoRoot, subdir.name);
80
+
81
+ // Check for package.json if not found yet
82
+ if (!result.packageJson) {
83
+ const packageJsonPath = path.join(subdirPath, 'package.json');
84
+ if (fs.existsSync(packageJsonPath)) {
85
+ result.packageJson = packageJsonPath;
86
+ logger.debug('version-manager - discoverVersionFiles', 'Found package.json in subdir', {
87
+ subdir: subdir.name
88
+ });
89
+ }
90
+ }
91
+
92
+ // Check for pom.xml if not found yet
93
+ if (!result.pomXml) {
94
+ const pomXmlPath = path.join(subdirPath, 'pom.xml');
95
+ if (fs.existsSync(pomXmlPath)) {
96
+ result.pomXml = pomXmlPath;
97
+ logger.debug('version-manager - discoverVersionFiles', 'Found pom.xml in subdir', {
98
+ subdir: subdir.name
99
+ });
100
+ }
101
+ }
102
+
103
+ // Stop searching if both found
104
+ if (result.packageJson && result.pomXml) {
105
+ break;
106
+ }
107
+ }
108
+ } catch (error) {
109
+ logger.debug('version-manager - discoverVersionFiles', 'Error searching subdirs', {
110
+ error: error.message
111
+ });
112
+ }
113
+
114
+ logger.debug('version-manager - discoverVersionFiles', 'Discovery complete', {
115
+ packageJson: result.packageJson ? path.relative(repoRoot, result.packageJson) : null,
116
+ pomXml: result.pomXml ? path.relative(repoRoot, result.pomXml) : null
117
+ });
118
+
119
+ return result;
120
+ }
121
+
28
122
  /**
29
123
  * Detects project type based on version files present
30
124
  * Why: Different projects require different version file handling
@@ -35,12 +129,11 @@ export function detectProjectType() {
35
129
  logger.debug('version-manager - detectProjectType', 'Detecting project type');
36
130
 
37
131
  try {
38
- const repoRoot = getRepoRoot();
39
- const packageJsonPath = path.join(repoRoot, 'package.json');
40
- const pomXmlPath = path.join(repoRoot, 'pom.xml');
132
+ // Discover version files (checks root and subdirectories)
133
+ discoveredPaths = discoverVersionFiles();
41
134
 
42
- const hasPackageJson = fs.existsSync(packageJsonPath);
43
- const hasPomXml = fs.existsSync(pomXmlPath);
135
+ const hasPackageJson = discoveredPaths.packageJson !== null;
136
+ const hasPomXml = discoveredPaths.pomXml !== null;
44
137
 
45
138
  let projectType;
46
139
  if (hasPackageJson && hasPomXml) {
@@ -53,10 +146,11 @@ export function detectProjectType() {
53
146
  projectType = PROJECT_TYPES.NONE;
54
147
  }
55
148
 
149
+ const repoRoot = getRepoRoot();
56
150
  logger.debug('version-manager - detectProjectType', 'Project type detected', {
57
151
  projectType,
58
- hasPackageJson,
59
- hasPomXml
152
+ packageJson: discoveredPaths.packageJson ? path.relative(repoRoot, discoveredPaths.packageJson) : null,
153
+ pomXml: discoveredPaths.pomXml ? path.relative(repoRoot, discoveredPaths.pomXml) : null
60
154
  });
61
155
 
62
156
  return projectType;
@@ -77,10 +171,9 @@ function readPackageJsonVersion() {
77
171
  logger.debug('version-manager - readPackageJsonVersion', 'Reading version from package.json');
78
172
 
79
173
  try {
80
- const repoRoot = getRepoRoot();
81
- const packageJsonPath = path.join(repoRoot, 'package.json');
174
+ const packageJsonPath = discoveredPaths.packageJson;
82
175
 
83
- if (!fs.existsSync(packageJsonPath)) {
176
+ if (!packageJsonPath || !fs.existsSync(packageJsonPath)) {
84
177
  logger.debug('version-manager - readPackageJsonVersion', 'package.json not found');
85
178
  return null;
86
179
  }
@@ -89,7 +182,10 @@ function readPackageJsonVersion() {
89
182
  const packageData = JSON.parse(content);
90
183
  const version = packageData.version || null;
91
184
 
92
- logger.debug('version-manager - readPackageJsonVersion', 'Version read', { version });
185
+ logger.debug('version-manager - readPackageJsonVersion', 'Version read', {
186
+ version,
187
+ path: packageJsonPath
188
+ });
93
189
 
94
190
  return version;
95
191
 
@@ -103,34 +199,56 @@ function readPackageJsonVersion() {
103
199
  * Reads current version from pom.xml
104
200
  * Why: Maven projects store version in pom.xml <project><version>
105
201
  *
202
+ * Strategy: Find <version> at nesting level 1 (direct child of <project>)
203
+ * - If <parent> block exists, look for <version> after </parent>
204
+ * - If no <parent> block, look for first <version> after <project>
205
+ *
106
206
  * @returns {string|null} Version string or null if not found
107
207
  */
108
208
  function readPomXmlVersion() {
109
209
  logger.debug('version-manager - readPomXmlVersion', 'Reading version from pom.xml');
110
210
 
111
211
  try {
112
- const repoRoot = getRepoRoot();
113
- const pomXmlPath = path.join(repoRoot, 'pom.xml');
212
+ const pomXmlPath = discoveredPaths.pomXml;
114
213
 
115
- if (!fs.existsSync(pomXmlPath)) {
214
+ if (!pomXmlPath || !fs.existsSync(pomXmlPath)) {
116
215
  logger.debug('version-manager - readPomXmlVersion', 'pom.xml not found');
117
216
  return null;
118
217
  }
119
218
 
120
219
  const content = fs.readFileSync(pomXmlPath, 'utf8');
121
220
 
122
- // Extract version from <project><version>X.Y.Z</version>
123
- // Why: Match only the project version, not parent or dependencies
124
- const projectVersionRegex = /<project[^>]*>[\s\S]*?<version>([^<]+)<\/version>/;
125
- const match = content.match(projectVersionRegex);
221
+ // Check if <parent> block exists
222
+ const hasParent = /<parent>[\s\S]*?<\/parent>/.test(content);
223
+ let version = null;
224
+
225
+ if (hasParent) {
226
+ // Find <version> after </parent> closing tag (project's direct version)
227
+ // Why: Parent's <version> is inside <parent>, project's <version> is after </parent>
228
+ const afterParentRegex = /<\/parent>[\s\S]*?<version>([^<]+)<\/version>/;
229
+ const match = content.match(afterParentRegex);
230
+ if (match) {
231
+ version = match[1].trim();
232
+ }
233
+ } else {
234
+ // No parent block - find first <version> after <project>
235
+ const projectVersionRegex = /<project[^>]*>[\s\S]*?<version>([^<]+)<\/version>/;
236
+ const match = content.match(projectVersionRegex);
237
+ if (match) {
238
+ version = match[1].trim();
239
+ }
240
+ }
126
241
 
127
- if (!match) {
242
+ if (!version) {
128
243
  logger.debug('version-manager - readPomXmlVersion', 'Version tag not found in pom.xml');
129
244
  return null;
130
245
  }
131
246
 
132
- const version = match[1].trim();
133
- logger.debug('version-manager - readPomXmlVersion', 'Version read', { version });
247
+ logger.debug('version-manager - readPomXmlVersion', 'Version read', {
248
+ version,
249
+ path: pomXmlPath,
250
+ hasParent
251
+ });
134
252
 
135
253
  return version;
136
254
 
@@ -303,20 +421,20 @@ export function incrementVersion(currentVersion, bumpType, suffix = null) {
303
421
 
304
422
  // Increment based on type
305
423
  switch (bumpType.toLowerCase()) {
306
- case 'major':
307
- parsed.major += 1;
308
- parsed.minor = 0;
309
- parsed.patch = 0;
310
- break;
311
- case 'minor':
312
- parsed.minor += 1;
313
- parsed.patch = 0;
314
- break;
315
- case 'patch':
316
- parsed.patch += 1;
317
- break;
318
- default:
319
- throw new Error(`Invalid bump type: ${bumpType}. Use major, minor, or patch.`);
424
+ case 'major':
425
+ parsed.major += 1;
426
+ parsed.minor = 0;
427
+ parsed.patch = 0;
428
+ break;
429
+ case 'minor':
430
+ parsed.minor += 1;
431
+ parsed.patch = 0;
432
+ break;
433
+ case 'patch':
434
+ parsed.patch += 1;
435
+ break;
436
+ default:
437
+ throw new Error(`Invalid bump type: ${bumpType}. Use major, minor, or patch.`);
320
438
  }
321
439
 
322
440
  // Construct new version
@@ -343,8 +461,11 @@ function updatePackageJsonVersion(newVersion) {
343
461
  logger.debug('version-manager - updatePackageJsonVersion', 'Updating package.json', { newVersion });
344
462
 
345
463
  try {
346
- const repoRoot = getRepoRoot();
347
- const packageJsonPath = path.join(repoRoot, 'package.json');
464
+ const packageJsonPath = discoveredPaths.packageJson;
465
+
466
+ if (!packageJsonPath) {
467
+ throw new Error('package.json path not discovered');
468
+ }
348
469
 
349
470
  const content = fs.readFileSync(packageJsonPath, 'utf8');
350
471
  const packageData = JSON.parse(content);
@@ -352,10 +473,12 @@ function updatePackageJsonVersion(newVersion) {
352
473
  packageData.version = newVersion;
353
474
 
354
475
  // Write with 4-space indent to match existing format
355
- const updatedContent = JSON.stringify(packageData, null, 4) + '\n';
476
+ const updatedContent = `${JSON.stringify(packageData, null, 4)}\n`;
356
477
  fs.writeFileSync(packageJsonPath, updatedContent, 'utf8');
357
478
 
358
- logger.debug('version-manager - updatePackageJsonVersion', 'package.json updated successfully');
479
+ logger.debug('version-manager - updatePackageJsonVersion', 'package.json updated', {
480
+ path: packageJsonPath
481
+ });
359
482
 
360
483
  } catch (error) {
361
484
  logger.error('version-manager - updatePackageJsonVersion', 'Failed to update package.json', error);
@@ -367,21 +490,38 @@ function updatePackageJsonVersion(newVersion) {
367
490
  * Updates version in pom.xml
368
491
  * Why: Writes new version to Maven project file
369
492
  *
493
+ * Strategy: Update <version> at nesting level 1 (direct child of <project>)
494
+ * - If <parent> block exists, update <version> after </parent>
495
+ * - If no <parent> block, update first <version> after <project>
496
+ *
370
497
  * @param {string} newVersion - New version string
371
498
  */
372
499
  function updatePomXmlVersion(newVersion) {
373
500
  logger.debug('version-manager - updatePomXmlVersion', 'Updating pom.xml', { newVersion });
374
501
 
375
502
  try {
376
- const repoRoot = getRepoRoot();
377
- const pomXmlPath = path.join(repoRoot, 'pom.xml');
503
+ const pomXmlPath = discoveredPaths.pomXml;
378
504
 
379
- let content = fs.readFileSync(pomXmlPath, 'utf8');
505
+ if (!pomXmlPath) {
506
+ throw new Error('pom.xml path not discovered');
507
+ }
380
508
 
381
- // Replace version in <project><version>X.Y.Z</version>
382
- // Why: Only update project version, not parent or dependencies
383
- const projectVersionRegex = /(<project[^>]*>[\s\S]*?<version>)[^<]+(<\/version>)/;
384
- const newContent = content.replace(projectVersionRegex, `$1${newVersion}$2`);
509
+ const content = fs.readFileSync(pomXmlPath, 'utf8');
510
+
511
+ // Check if <parent> block exists
512
+ const hasParent = /<parent>[\s\S]*?<\/parent>/.test(content);
513
+ let newContent;
514
+
515
+ if (hasParent) {
516
+ // Replace <version> after </parent> closing tag (project's direct version)
517
+ // Why: Parent's <version> is inside <parent>, project's <version> is after </parent>
518
+ const afterParentRegex = /(<\/parent>[\s\S]*?<version>)[^<]+(<\/version>)/;
519
+ newContent = content.replace(afterParentRegex, `$1${newVersion}$2`);
520
+ } else {
521
+ // No parent block - replace first <version> after <project>
522
+ const projectVersionRegex = /(<project[^>]*>[\s\S]*?<version>)[^<]+(<\/version>)/;
523
+ newContent = content.replace(projectVersionRegex, `$1${newVersion}$2`);
524
+ }
385
525
 
386
526
  if (content === newContent) {
387
527
  logger.warning('version-manager - updatePomXmlVersion', 'No version tag found to update');
@@ -390,7 +530,10 @@ function updatePomXmlVersion(newVersion) {
390
530
 
391
531
  fs.writeFileSync(pomXmlPath, newContent, 'utf8');
392
532
 
393
- logger.debug('version-manager - updatePomXmlVersion', 'pom.xml updated successfully');
533
+ logger.debug('version-manager - updatePomXmlVersion', 'pom.xml updated', {
534
+ path: pomXmlPath,
535
+ hasParent
536
+ });
394
537
 
395
538
  } catch (error) {
396
539
  logger.error('version-manager - updatePomXmlVersion', 'Failed to update pom.xml', error);
@@ -460,11 +603,20 @@ export function validateVersionFormat(version) {
460
603
  *
461
604
  * @param {string} version1 - First version
462
605
  * @param {string} version2 - Second version
463
- * @returns {number} 1 if v1 > v2, -1 if v1 < v2, 0 if equal
606
+ * @returns {number} 1 if v1 > v2, -1 if v1 < v2, 0 if equal or invalid
464
607
  */
465
608
  export function compareVersions(version1, version2) {
466
609
  logger.debug('version-manager - compareVersions', 'Comparing versions', { version1, version2 });
467
610
 
611
+ // Handle null/undefined versions gracefully
612
+ if (!version1 || !version2) {
613
+ logger.debug('version-manager - compareVersions', 'Missing version, returning 0', {
614
+ version1,
615
+ version2
616
+ });
617
+ return 0;
618
+ }
619
+
468
620
  try {
469
621
  const v1 = parseVersion(version1);
470
622
  const v2 = parseVersion(version2);
@@ -489,7 +641,12 @@ export function compareVersions(version1, version2) {
489
641
  return 0;
490
642
 
491
643
  } catch (error) {
492
- logger.error('version-manager - compareVersions', 'Failed to compare versions', error);
644
+ // Log at debug level instead of error - non-semver versions are expected
645
+ logger.debug('version-manager - compareVersions', 'Cannot compare versions (not semver)', {
646
+ version1,
647
+ version2,
648
+ error: error.message
649
+ });
493
650
  return 0;
494
651
  }
495
652
  }
@@ -507,7 +664,7 @@ export async function validateVersionAlignment() {
507
664
  const projectType = detectProjectType();
508
665
  const versions = getCurrentVersion(projectType);
509
666
 
510
- // Get git tag version
667
+ // Get git tag version (parseTagVersion returns null for non-semver tags)
511
668
  const { getLatestLocalTag, parseTagVersion } = await import('./git-tag-manager.js');
512
669
  const latestTag = getLatestLocalTag();
513
670
  const tagVersion = latestTag ? parseTagVersion(latestTag) : null;
@@ -515,7 +672,7 @@ export async function validateVersionAlignment() {
515
672
  // Get CHANGELOG version
516
673
  const changelogVersion = readChangelogVersion();
517
674
 
518
- // Get remote tag version
675
+ // Get remote tag version (parseTagVersion returns null for non-semver tags)
519
676
  const { getLatestRemoteTag } = await import('./git-tag-manager.js');
520
677
  const latestRemoteTag = await getLatestRemoteTag();
521
678
  const remoteVersion = latestRemoteTag ? parseTagVersion(latestRemoteTag) : null;
@@ -581,3 +738,13 @@ export async function validateVersionAlignment() {
581
738
  };
582
739
  }
583
740
  }
741
+
742
+ /**
743
+ * Gets discovered version file paths
744
+ * Why: Allows callers to display which files will be updated
745
+ *
746
+ * @returns {Object} Discovered paths: { packageJson, pomXml }
747
+ */
748
+ export function getDiscoveredPaths() {
749
+ return { ...discoveredPaths };
750
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-git-hooks",
3
- "version": "2.12.0",
3
+ "version": "2.14.1",
4
4
  "description": "Git hooks with Claude CLI for code analysis and automatic commit messages",
5
5
  "type": "module",
6
6
  "bin": {