claude-git-hooks 2.14.4 → 2.16.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +116 -5
  2. package/LICENSE +20 -20
  3. package/bin/claude-hooks +84 -84
  4. package/lib/commands/analyze-diff.js +0 -1
  5. package/lib/commands/bump-version.js +346 -114
  6. package/lib/commands/create-pr.js +1 -1
  7. package/lib/commands/debug.js +1 -1
  8. package/lib/commands/generate-changelog.js +5 -6
  9. package/lib/commands/helpers.js +11 -6
  10. package/lib/commands/install.js +7 -8
  11. package/lib/commands/migrate-config.js +24 -2
  12. package/lib/commands/setup-github.js +1 -1
  13. package/lib/commands/update.js +1 -1
  14. package/lib/config.js +0 -4
  15. package/lib/hooks/prepare-commit-msg.js +2 -2
  16. package/lib/utils/changelog-generator.js +6 -8
  17. package/lib/utils/claude-client.js +7 -6
  18. package/lib/utils/claude-diagnostics.js +14 -21
  19. package/lib/utils/file-operations.js +1 -1
  20. package/lib/utils/file-utils.js +0 -1
  21. package/lib/utils/git-operations.js +0 -1
  22. package/lib/utils/github-api.js +3 -3
  23. package/lib/utils/github-client.js +2 -2
  24. package/lib/utils/installation-diagnostics.js +1 -1
  25. package/lib/utils/interactive-ui.js +93 -0
  26. package/lib/utils/prompt-builder.js +4 -6
  27. package/lib/utils/sanitize.js +13 -14
  28. package/lib/utils/task-id.js +18 -20
  29. package/lib/utils/telemetry.js +5 -7
  30. package/lib/utils/version-manager.js +676 -296
  31. package/package.json +68 -60
  32. package/templates/config.advanced.example.json +113 -113
  33. package/templates/pre-commit +7 -0
  34. package/templates/presets/ai/config.json +5 -5
  35. package/templates/presets/backend/config.json +5 -5
  36. package/templates/presets/backend/preset.json +49 -49
  37. package/templates/presets/database/config.json +5 -5
  38. package/templates/presets/database/preset.json +38 -38
  39. package/templates/presets/default/config.json +5 -5
  40. package/templates/presets/default/preset.json +53 -53
  41. package/templates/presets/frontend/config.json +5 -5
  42. package/templates/presets/frontend/preset.json +50 -50
  43. package/templates/presets/fullstack/config.json +5 -5
  44. package/templates/presets/fullstack/preset.json +55 -55
@@ -16,245 +16,627 @@ import { getRepoRoot } from './git-operations.js';
16
16
  import logger from './logger.js';
17
17
 
18
18
  /**
19
- * Project types supported by version manager
19
+ * Cache for discovery result
20
+ * Why: Store full discovery result for access by getDiscoveryResult()
20
21
  */
21
- const PROJECT_TYPES = {
22
- NODE: 'node',
23
- MAVEN: 'maven',
24
- BOTH: 'both',
25
- NONE: 'none'
26
- };
22
+ let cachedDiscoveryResult = null;
27
23
 
28
24
  /**
29
- * Cache for discovered version file paths
30
- * Why: Avoid re-discovering paths on every operation
25
+ * Registry of supported version file types
26
+ * Why: Extensible system for multi-language version management
27
+ *
28
+ * Each entry defines:
29
+ * - filename: The file to search for
30
+ * - readVersion: Function to extract version from file
31
+ * - writeVersion: Function to update version in file
32
+ * - projectLabel: Human-readable project type label
31
33
  */
32
- let discoveredPaths = {
33
- packageJson: null,
34
- pomXml: null
34
+ const VERSION_FILE_TYPES = {
35
+ 'package.json': {
36
+ filename: 'package.json',
37
+ readVersion: (filePath) => {
38
+ try {
39
+ const content = fs.readFileSync(filePath, 'utf8');
40
+ const packageData = JSON.parse(content);
41
+ return packageData.version || null;
42
+ } catch (error) {
43
+ logger.debug('version-manager - VERSION_FILE_TYPES', 'Failed to read package.json', {
44
+ filePath,
45
+ error: error.message
46
+ });
47
+ return null;
48
+ }
49
+ },
50
+ writeVersion: (filePath, newVersion) => {
51
+ const content = fs.readFileSync(filePath, 'utf8');
52
+ const packageData = JSON.parse(content);
53
+ packageData.version = newVersion;
54
+ const updatedContent = `${JSON.stringify(packageData, null, 4)}\n`;
55
+ fs.writeFileSync(filePath, updatedContent, 'utf8');
56
+ },
57
+ projectLabel: 'Node.js'
58
+ },
59
+ 'pom.xml': {
60
+ filename: 'pom.xml',
61
+ readVersion: (filePath) => {
62
+ try {
63
+ 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
+ }
75
+ } catch (error) {
76
+ logger.debug('version-manager - VERSION_FILE_TYPES', 'Failed to read pom.xml', {
77
+ filePath,
78
+ error: error.message
79
+ });
80
+ return null;
81
+ }
82
+ },
83
+ writeVersion: (filePath, newVersion) => {
84
+ const content = fs.readFileSync(filePath, 'utf8');
85
+ const hasParent = /<parent>[\s\S]*?<\/parent>/.test(content);
86
+
87
+ let newContent;
88
+ if (hasParent) {
89
+ const afterParentRegex = /(<\/parent>[\s\S]*?<version>)[^<]+(<\/version>)/;
90
+ newContent = content.replace(afterParentRegex, `$1${newVersion}$2`);
91
+ } else {
92
+ const projectVersionRegex = /(<project[^>]*>[\s\S]*?<version>)[^<]+(<\/version>)/;
93
+ newContent = content.replace(projectVersionRegex, `$1${newVersion}$2`);
94
+ }
95
+
96
+ if (content === newContent) {
97
+ throw new Error('Could not find <version> tag in pom.xml');
98
+ }
99
+
100
+ fs.writeFileSync(filePath, newContent, 'utf8');
101
+ },
102
+ projectLabel: 'Maven'
103
+ },
104
+ 'build.gradle': {
105
+ filename: 'build.gradle',
106
+ readVersion: readGradleVersion,
107
+ writeVersion: updateGradleVersion,
108
+ projectLabel: 'Gradle'
109
+ },
110
+ 'build.gradle.kts': {
111
+ filename: 'build.gradle.kts',
112
+ readVersion: readGradleKtsVersion,
113
+ writeVersion: updateGradleKtsVersion,
114
+ projectLabel: 'Gradle (Kotlin)'
115
+ },
116
+ 'pyproject.toml': {
117
+ filename: 'pyproject.toml',
118
+ readVersion: readPyprojectVersion,
119
+ writeVersion: updatePyprojectVersion,
120
+ projectLabel: 'Python'
121
+ },
122
+ 'Cargo.toml': {
123
+ filename: 'Cargo.toml',
124
+ readVersion: readCargoVersion,
125
+ writeVersion: updateCargoVersion,
126
+ projectLabel: 'Rust'
127
+ },
128
+ 'version.sbt': {
129
+ filename: 'version.sbt',
130
+ readVersion: readSbtVersion,
131
+ writeVersion: updateSbtVersion,
132
+ projectLabel: 'Scala/sbt'
133
+ }
35
134
  };
36
135
 
37
136
  /**
38
- * Discovers version files in repo root and one level deep
39
- * Why: Support monorepos with version files in subdirectories
137
+ * Discovers all version files recursively in repository
138
+ * Why: Support monorepos with multiple version files of same/different types
40
139
  *
41
- * @returns {Object} Discovered paths: { packageJson, pomXml }
140
+ * @param {Object} options - Discovery options
141
+ * @param {number} options.maxDepth - Maximum directory depth to search (default: 3)
142
+ * @param {string[]} options.fileTypes - File types to search for (default: all types in registry)
143
+ * @param {string[]} options.ignoreDirs - Additional directories to ignore
144
+ * @returns {Object} DiscoveryResult: { files, resolvedVersion, mismatch, types }
42
145
  */
43
- export function discoverVersionFiles() {
44
- logger.debug('version-manager - discoverVersionFiles', 'Searching for version files');
146
+ export function discoverVersionFiles(options = {}) {
147
+ const {
148
+ maxDepth = 3,
149
+ fileTypes = Object.keys(VERSION_FILE_TYPES),
150
+ ignoreDirs = []
151
+ } = options;
152
+
153
+ logger.debug('version-manager - discoverVersionFiles', 'Starting discovery', {
154
+ maxDepth,
155
+ fileTypes,
156
+ ignoreDirs
157
+ });
45
158
 
46
159
  const repoRoot = getRepoRoot();
47
- const result = { packageJson: null, pomXml: null };
160
+ const discoveredFiles = [];
161
+
162
+ // Default ignore patterns
163
+ const defaultIgnore = [
164
+ '.git',
165
+ 'node_modules',
166
+ 'target',
167
+ 'build',
168
+ 'dist',
169
+ '__pycache__',
170
+ '.venv',
171
+ 'vendor',
172
+ 'out',
173
+ '.next',
174
+ '.nuxt',
175
+ 'coverage'
176
+ ];
177
+ const ignoreSet = new Set([...defaultIgnore, ...ignoreDirs]);
178
+
179
+ /**
180
+ * Recursively walks directory tree
181
+ * @param {string} dir - Current directory path
182
+ * @param {number} depth - Current depth level
183
+ */
184
+ function walkDirectory(dir, depth) {
185
+ if (depth > maxDepth) {
186
+ return;
187
+ }
48
188
 
49
- // First check root level
50
- const rootPackageJson = path.join(repoRoot, 'package.json');
51
- const rootPomXml = path.join(repoRoot, 'pom.xml');
189
+ try {
190
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
52
191
 
53
- if (fs.existsSync(rootPackageJson)) {
54
- result.packageJson = rootPackageJson;
192
+ for (const entry of entries) {
193
+ // Skip hidden directories and ignored patterns
194
+ if (entry.name.startsWith('.') || ignoreSet.has(entry.name)) {
195
+ continue;
196
+ }
197
+
198
+ const fullPath = path.join(dir, entry.name);
199
+
200
+ if (entry.isDirectory()) {
201
+ // Recurse into subdirectory
202
+ walkDirectory(fullPath, depth + 1);
203
+ } else if (entry.isFile()) {
204
+ // Check if this file matches any registered type
205
+ for (const fileType of fileTypes) {
206
+ const registry = VERSION_FILE_TYPES[fileType];
207
+ if (registry && entry.name === registry.filename) {
208
+ // Read version from file
209
+ const version = registry.readVersion(fullPath);
210
+
211
+ // Create descriptor
212
+ const descriptor = {
213
+ path: fullPath,
214
+ relativePath: path.relative(repoRoot, fullPath),
215
+ type: fileType,
216
+ projectLabel: registry.projectLabel,
217
+ version,
218
+ selected: true // Default: all files selected
219
+ };
220
+
221
+ discoveredFiles.push(descriptor);
222
+
223
+ logger.debug('version-manager - discoverVersionFiles', 'Found version file', {
224
+ relativePath: descriptor.relativePath,
225
+ type: fileType,
226
+ version
227
+ });
228
+ }
229
+ }
230
+ }
231
+ }
232
+ } catch (error) {
233
+ logger.debug('version-manager - discoverVersionFiles', 'Error reading directory', {
234
+ dir,
235
+ error: error.message
236
+ });
237
+ }
55
238
  }
56
- if (fs.existsSync(rootPomXml)) {
57
- result.pomXml = rootPomXml;
239
+
240
+ // Start walking from repo root at depth 0
241
+ walkDirectory(repoRoot, 0);
242
+
243
+ // Sort results: root files first, then by depth, then alphabetically
244
+ discoveredFiles.sort((a, b) => {
245
+ const depthA = a.relativePath.split(path.sep).length;
246
+ const depthB = b.relativePath.split(path.sep).length;
247
+
248
+ if (depthA !== depthB) {
249
+ return depthA - depthB;
250
+ }
251
+
252
+ return a.relativePath.localeCompare(b.relativePath);
253
+ });
254
+
255
+ // Determine resolved version (prefer root-level file, then first found)
256
+ let resolvedVersion = null;
257
+ const rootFile = discoveredFiles.find(f => !f.relativePath.includes(path.sep));
258
+ if (rootFile && rootFile.version) {
259
+ resolvedVersion = rootFile.version;
260
+ } else if (discoveredFiles.length > 0) {
261
+ // Use first file's version as fallback
262
+ const firstWithVersion = discoveredFiles.find(f => f.version !== null);
263
+ resolvedVersion = firstWithVersion ? firstWithVersion.version : null;
58
264
  }
59
265
 
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;
266
+ // Check for version mismatch
267
+ const versions = discoveredFiles
268
+ .filter(f => f.version !== null)
269
+ .map(f => f.version);
270
+ const uniqueVersions = [...new Set(versions)];
271
+ const mismatch = uniqueVersions.length > 1;
272
+
273
+ // Collect unique types
274
+ const types = [...new Set(discoveredFiles.map(f => f.type))];
275
+
276
+ const result = {
277
+ files: discoveredFiles,
278
+ resolvedVersion,
279
+ mismatch,
280
+ types
281
+ };
282
+
283
+ // Cache the result
284
+ cachedDiscoveryResult = result;
285
+
286
+ logger.debug('version-manager - discoverVersionFiles', 'Discovery complete', {
287
+ filesFound: discoveredFiles.length,
288
+ resolvedVersion,
289
+ mismatch,
290
+ types
291
+ });
292
+
293
+ return result;
294
+ }
295
+
296
+ /**
297
+ * Gets cached discovery result
298
+ * Why: Allows access to full discovery result without re-scanning
299
+ *
300
+ * @returns {Object|null} Cached DiscoveryResult or null
301
+ */
302
+ export function getDiscoveryResult() {
303
+ return cachedDiscoveryResult;
304
+ }
305
+
306
+ /**
307
+ * Reads version from any supported file type
308
+ * Why: Unified interface for reading versions
309
+ *
310
+ * @param {string} filePath - Path to version file
311
+ * @param {string} type - File type key from VERSION_FILE_TYPES
312
+ * @returns {string|null} Version string or null
313
+ */
314
+ export function readVersionFromFile(filePath, type) {
315
+ logger.debug('version-manager - readVersionFromFile', 'Reading version', { filePath, type });
316
+
317
+ const registry = VERSION_FILE_TYPES[type];
318
+ if (!registry) {
319
+ throw new Error(`Unsupported file type: ${type}`);
64
320
  }
65
321
 
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
- );
322
+ return registry.readVersion(filePath);
323
+ }
77
324
 
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
- }
325
+ /**
326
+ * Writes version to any supported file type
327
+ * Why: Unified interface for writing versions
328
+ *
329
+ * @param {string} filePath - Path to version file
330
+ * @param {string} type - File type key from VERSION_FILE_TYPES
331
+ * @param {string} newVersion - New version string
332
+ */
333
+ export function writeVersionToFile(filePath, type, newVersion) {
334
+ logger.debug('version-manager - writeVersionToFile', 'Writing version', {
335
+ filePath,
336
+ type,
337
+ newVersion
338
+ });
91
339
 
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
- }
340
+ const registry = VERSION_FILE_TYPES[type];
341
+ if (!registry) {
342
+ throw new Error(`Unsupported file type: ${type}`);
343
+ }
102
344
 
103
- // Stop searching if both found
104
- if (result.packageJson && result.pomXml) {
105
- break;
345
+ registry.writeVersion(filePath, newVersion);
346
+ }
347
+
348
+ /**
349
+ * Updates version in selected files
350
+ * Why: Applies version update to specific VersionFileDescriptor[] subset
351
+ *
352
+ * @param {Array} files - Array of VersionFileDescriptor objects with selected=true
353
+ * @param {string} newVersion - New version string
354
+ */
355
+ export function updateVersionFiles(files, newVersion) {
356
+ logger.debug('version-manager - updateVersionFiles', 'Updating version files', {
357
+ fileCount: files.length,
358
+ newVersion
359
+ });
360
+
361
+ // Validate version format first
362
+ parseVersion(newVersion); // Throws if invalid
363
+
364
+ // Validate per-file target versions
365
+ for (const file of files) {
366
+ if (file.targetVersion) {
367
+ parseVersion(file.targetVersion); // Throws if invalid
368
+ }
369
+ }
370
+
371
+ const errors = [];
372
+
373
+ for (const file of files) {
374
+ if (!file.selected) {
375
+ continue; // Skip unselected files
376
+ }
377
+
378
+ try {
379
+ // Verify file still exists
380
+ if (!fs.existsSync(file.path)) {
381
+ throw new Error(`File not found: ${file.path}`);
106
382
  }
383
+
384
+ // Use per-file target version if set, otherwise global newVersion
385
+ const targetVersion = file.targetVersion || newVersion;
386
+ writeVersionToFile(file.path, file.type, targetVersion);
387
+
388
+ logger.debug('version-manager - updateVersionFiles', 'File updated', {
389
+ path: file.relativePath,
390
+ type: file.type,
391
+ version: targetVersion
392
+ });
393
+
394
+ } catch (error) {
395
+ logger.error('version-manager - updateVersionFiles', 'Failed to update file', {
396
+ path: file.relativePath,
397
+ error: error.message
398
+ });
399
+ errors.push({ file: file.relativePath, error: error.message });
107
400
  }
108
- } catch (error) {
109
- logger.debug('version-manager - discoverVersionFiles', 'Error searching subdirs', {
110
- error: error.message
111
- });
112
401
  }
113
402
 
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
403
+ if (errors.length > 0) {
404
+ const errorMsg = errors.map(e => `${e.file}: ${e.error}`).join(', ');
405
+ throw new Error(`Failed to update some files: ${errorMsg}`);
406
+ }
407
+
408
+ logger.debug('version-manager - updateVersionFiles', 'All files updated successfully');
409
+ }
410
+
411
+ /**
412
+ * Modifies version suffix without bumping version numbers
413
+ * Why: Support suffix-only operations (add/remove/replace SNAPSHOT, RC, etc.)
414
+ *
415
+ * @param {string} version - Current version string
416
+ * @param {Object} options - Modification options
417
+ * @param {boolean} options.remove - Remove existing suffix
418
+ * @param {string} options.set - Set/replace suffix with this value
419
+ * @returns {string} Modified version string
420
+ */
421
+ export function modifySuffix(version, options = {}) {
422
+ logger.debug('version-manager - modifySuffix', 'Modifying suffix', { version, options });
423
+
424
+ const parsed = parseVersion(version);
425
+
426
+ let newVersion;
427
+
428
+ if (options.remove) {
429
+ // Remove suffix: 2.15.5-SNAPSHOT → 2.15.5
430
+ newVersion = `${parsed.major}.${parsed.minor}.${parsed.patch}`;
431
+ } else if (options.set) {
432
+ // Set/replace suffix: 2.15.5 → 2.15.5-SNAPSHOT or 2.15.5-RC1 → 2.15.5-RC2
433
+ newVersion = `${parsed.major}.${parsed.minor}.${parsed.patch}-${options.set}`;
434
+ } else {
435
+ // No-op: return original
436
+ newVersion = version;
437
+ }
438
+
439
+ logger.debug('version-manager - modifySuffix', 'Suffix modified', {
440
+ oldVersion: version,
441
+ newVersion
117
442
  });
118
443
 
119
- return result;
444
+ return newVersion;
120
445
  }
121
446
 
122
447
  /**
123
- * Detects project type based on version files present
124
- * Why: Different projects require different version file handling
448
+ * Reads current version from build.gradle
449
+ * Why: Gradle projects store version in version = '...' or version '...'
125
450
  *
126
- * @returns {string} Project type: 'node' | 'maven' | 'both' | 'none'
451
+ * @param {string} filePath - Path to build.gradle
452
+ * @returns {string|null} Version string or null if not found
127
453
  */
128
- export function detectProjectType() {
129
- logger.debug('version-manager - detectProjectType', 'Detecting project type');
454
+ function readGradleVersion(filePath) {
455
+ logger.debug('version-manager - readGradleVersion', 'Reading version from build.gradle', { filePath });
130
456
 
131
457
  try {
132
- // Discover version files (checks root and subdirectories)
133
- discoveredPaths = discoverVersionFiles();
458
+ if (!fs.existsSync(filePath)) {
459
+ logger.debug('version-manager - readGradleVersion', 'build.gradle not found');
460
+ return null;
461
+ }
462
+
463
+ const content = fs.readFileSync(filePath, 'utf8');
134
464
 
135
- const hasPackageJson = discoveredPaths.packageJson !== null;
136
- const hasPomXml = discoveredPaths.pomXml !== null;
465
+ // Match: version = '...' or version '...' or version="..." or version "..."
466
+ const versionRegex = /version\s*=?\s*['"]([^'"]+)['"]/;
467
+ const match = content.match(versionRegex);
137
468
 
138
- let projectType;
139
- if (hasPackageJson && hasPomXml) {
140
- projectType = PROJECT_TYPES.BOTH;
141
- } else if (hasPackageJson) {
142
- projectType = PROJECT_TYPES.NODE;
143
- } else if (hasPomXml) {
144
- projectType = PROJECT_TYPES.MAVEN;
145
- } else {
146
- projectType = PROJECT_TYPES.NONE;
469
+ if (!match) {
470
+ logger.debug('version-manager - readGradleVersion', 'Version not found in build.gradle');
471
+ return null;
147
472
  }
148
473
 
149
- const repoRoot = getRepoRoot();
150
- logger.debug('version-manager - detectProjectType', 'Project type detected', {
151
- projectType,
152
- packageJson: discoveredPaths.packageJson ? path.relative(repoRoot, discoveredPaths.packageJson) : null,
153
- pomXml: discoveredPaths.pomXml ? path.relative(repoRoot, discoveredPaths.pomXml) : null
154
- });
474
+ const version = match[1].trim();
475
+ logger.debug('version-manager - readGradleVersion', 'Version read', { version, path: filePath });
155
476
 
156
- return projectType;
477
+ return version;
157
478
 
158
479
  } catch (error) {
159
- logger.error('version-manager - detectProjectType', 'Failed to detect project type', error);
160
- throw error;
480
+ logger.error('version-manager - readGradleVersion', 'Failed to read version', error);
481
+ throw new Error(`Failed to read build.gradle: ${error.message}`);
161
482
  }
162
483
  }
163
484
 
164
485
  /**
165
- * Reads current version from package.json
166
- * Why: Node.js projects store version in package.json
486
+ * Reads current version from build.gradle.kts (Kotlin DSL)
487
+ * Why: Gradle Kotlin DSL uses double quotes
167
488
  *
489
+ * @param {string} filePath - Path to build.gradle.kts
168
490
  * @returns {string|null} Version string or null if not found
169
491
  */
170
- function readPackageJsonVersion() {
171
- logger.debug('version-manager - readPackageJsonVersion', 'Reading version from package.json');
492
+ function readGradleKtsVersion(filePath) {
493
+ logger.debug('version-manager - readGradleKtsVersion', 'Reading version from build.gradle.kts', { filePath });
172
494
 
173
495
  try {
174
- const packageJsonPath = discoveredPaths.packageJson;
175
-
176
- if (!packageJsonPath || !fs.existsSync(packageJsonPath)) {
177
- logger.debug('version-manager - readPackageJsonVersion', 'package.json not found');
496
+ if (!fs.existsSync(filePath)) {
497
+ logger.debug('version-manager - readGradleKtsVersion', 'build.gradle.kts not found');
178
498
  return null;
179
499
  }
180
500
 
181
- const content = fs.readFileSync(packageJsonPath, 'utf8');
182
- const packageData = JSON.parse(content);
183
- const version = packageData.version || null;
501
+ const content = fs.readFileSync(filePath, 'utf8');
184
502
 
185
- logger.debug('version-manager - readPackageJsonVersion', 'Version read', {
186
- version,
187
- path: packageJsonPath
188
- });
503
+ // Match: version = "..."
504
+ const versionRegex = /version\s*=\s*"([^"]+)"/;
505
+ const match = content.match(versionRegex);
506
+
507
+ if (!match) {
508
+ logger.debug('version-manager - readGradleKtsVersion', 'Version not found in build.gradle.kts');
509
+ return null;
510
+ }
511
+
512
+ const version = match[1].trim();
513
+ logger.debug('version-manager - readGradleKtsVersion', 'Version read', { version, path: filePath });
189
514
 
190
515
  return version;
191
516
 
192
517
  } catch (error) {
193
- logger.error('version-manager - readPackageJsonVersion', 'Failed to read version', error);
194
- throw new Error(`Failed to read package.json: ${error.message}`);
518
+ logger.error('version-manager - readGradleKtsVersion', 'Failed to read version', error);
519
+ throw new Error(`Failed to read build.gradle.kts: ${error.message}`);
195
520
  }
196
521
  }
197
522
 
198
523
  /**
199
- * Reads current version from pom.xml
200
- * Why: Maven projects store version in pom.xml <project><version>
524
+ * Reads current version from pyproject.toml
525
+ * Why: Python projects use pyproject.toml with [project] or [tool.poetry] section
201
526
  *
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>
527
+ * @param {string} filePath - Path to pyproject.toml
528
+ * @returns {string|null} Version string or null if not found
529
+ */
530
+ function readPyprojectVersion(filePath) {
531
+ logger.debug('version-manager - readPyprojectVersion', 'Reading version from pyproject.toml', { filePath });
532
+
533
+ try {
534
+ if (!fs.existsSync(filePath)) {
535
+ logger.debug('version-manager - readPyprojectVersion', 'pyproject.toml not found');
536
+ return null;
537
+ }
538
+
539
+ const content = fs.readFileSync(filePath, 'utf8');
540
+
541
+ // Try [project] section first (PEP 621)
542
+ const projectSectionRegex = /\[project\][\s\S]*?version\s*=\s*["']([^"']+)["']/;
543
+ let match = content.match(projectSectionRegex);
544
+
545
+ // If not found, try [tool.poetry] section
546
+ if (!match) {
547
+ const poetrySectionRegex = /\[tool\.poetry\][\s\S]*?version\s*=\s*["']([^"']+)["']/;
548
+ match = content.match(poetrySectionRegex);
549
+ }
550
+
551
+ if (!match) {
552
+ logger.debug('version-manager - readPyprojectVersion', 'Version not found in pyproject.toml');
553
+ return null;
554
+ }
555
+
556
+ const version = match[1].trim();
557
+ logger.debug('version-manager - readPyprojectVersion', 'Version read', { version, path: filePath });
558
+
559
+ return version;
560
+
561
+ } catch (error) {
562
+ logger.error('version-manager - readPyprojectVersion', 'Failed to read version', error);
563
+ throw new Error(`Failed to read pyproject.toml: ${error.message}`);
564
+ }
565
+ }
566
+
567
+ /**
568
+ * Reads current version from Cargo.toml
569
+ * Why: Rust projects store version in [package] section
205
570
  *
571
+ * @param {string} filePath - Path to Cargo.toml
206
572
  * @returns {string|null} Version string or null if not found
207
573
  */
208
- function readPomXmlVersion() {
209
- logger.debug('version-manager - readPomXmlVersion', 'Reading version from pom.xml');
574
+ function readCargoVersion(filePath) {
575
+ logger.debug('version-manager - readCargoVersion', 'Reading version from Cargo.toml', { filePath });
210
576
 
211
577
  try {
212
- const pomXmlPath = discoveredPaths.pomXml;
578
+ if (!fs.existsSync(filePath)) {
579
+ logger.debug('version-manager - readCargoVersion', 'Cargo.toml not found');
580
+ return null;
581
+ }
582
+
583
+ const content = fs.readFileSync(filePath, 'utf8');
213
584
 
214
- if (!pomXmlPath || !fs.existsSync(pomXmlPath)) {
215
- logger.debug('version-manager - readPomXmlVersion', 'pom.xml not found');
585
+ // Match version in [package] section
586
+ const versionRegex = /\[package\][\s\S]*?version\s*=\s*["']([^"']+)["']/;
587
+ const match = content.match(versionRegex);
588
+
589
+ if (!match) {
590
+ logger.debug('version-manager - readCargoVersion', 'Version not found in Cargo.toml');
216
591
  return null;
217
592
  }
218
593
 
219
- const content = fs.readFileSync(pomXmlPath, 'utf8');
594
+ const version = match[1].trim();
595
+ logger.debug('version-manager - readCargoVersion', 'Version read', { version, path: filePath });
220
596
 
221
- // Check if <parent> block exists
222
- const hasParent = /<parent>[\s\S]*?<\/parent>/.test(content);
223
- let version = null;
597
+ return version;
224
598
 
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
- }
599
+ } catch (error) {
600
+ logger.error('version-manager - readCargoVersion', 'Failed to read version', error);
601
+ throw new Error(`Failed to read Cargo.toml: ${error.message}`);
602
+ }
603
+ }
604
+
605
+ /**
606
+ * Reads current version from version.sbt
607
+ * Why: Scala/sbt projects store version in ThisBuild / version := "..." or version := "..."
608
+ *
609
+ * @param {string} filePath - Path to version.sbt
610
+ * @returns {string|null} Version string or null if not found
611
+ */
612
+ function readSbtVersion(filePath) {
613
+ logger.debug('version-manager - readSbtVersion', 'Reading version from version.sbt', { filePath });
614
+
615
+ try {
616
+ if (!fs.existsSync(filePath)) {
617
+ logger.debug('version-manager - readSbtVersion', 'version.sbt not found');
618
+ return null;
240
619
  }
241
620
 
242
- if (!version) {
243
- logger.debug('version-manager - readPomXmlVersion', 'Version tag not found in pom.xml');
621
+ const content = fs.readFileSync(filePath, 'utf8');
622
+
623
+ // Match: ThisBuild / version := "..." or version := "..."
624
+ const versionRegex = /(?:ThisBuild\s*\/\s*)?version\s*:=\s*["']([^"']+)["']/;
625
+ const match = content.match(versionRegex);
626
+
627
+ if (!match) {
628
+ logger.debug('version-manager - readSbtVersion', 'Version not found in version.sbt');
244
629
  return null;
245
630
  }
246
631
 
247
- logger.debug('version-manager - readPomXmlVersion', 'Version read', {
248
- version,
249
- path: pomXmlPath,
250
- hasParent
251
- });
632
+ const version = match[1].trim();
633
+ logger.debug('version-manager - readSbtVersion', 'Version read', { version, path: filePath });
252
634
 
253
635
  return version;
254
636
 
255
637
  } catch (error) {
256
- logger.error('version-manager - readPomXmlVersion', 'Failed to read version', error);
257
- throw new Error(`Failed to read pom.xml: ${error.message}`);
638
+ logger.error('version-manager - readSbtVersion', 'Failed to read version', error);
639
+ throw new Error(`Failed to read version.sbt: ${error.message}`);
258
640
  }
259
641
  }
260
642
 
@@ -315,57 +697,6 @@ function readChangelogVersion() {
315
697
  }
316
698
  }
317
699
 
318
- /**
319
- * Gets current version based on project type
320
- * Why: Abstracts version location from caller
321
- *
322
- * @param {string} projectType - Project type from detectProjectType()
323
- * @returns {Object} Version info: { packageJson, pomXml, resolved, mismatch }
324
- */
325
- export function getCurrentVersion(projectType) {
326
- logger.debug('version-manager - getCurrentVersion', 'Getting current version', { projectType });
327
-
328
- const versions = {
329
- packageJson: null,
330
- pomXml: null,
331
- resolved: null,
332
- mismatch: false
333
- };
334
-
335
- try {
336
- if (projectType === PROJECT_TYPES.NODE || projectType === PROJECT_TYPES.BOTH) {
337
- versions.packageJson = readPackageJsonVersion();
338
- }
339
-
340
- if (projectType === PROJECT_TYPES.MAVEN || projectType === PROJECT_TYPES.BOTH) {
341
- versions.pomXml = readPomXmlVersion();
342
- }
343
-
344
- // Resolve version: use package.json if available, otherwise pom.xml
345
- versions.resolved = versions.packageJson || versions.pomXml;
346
-
347
- // Validate consistency in monorepos
348
- if (projectType === PROJECT_TYPES.BOTH) {
349
- if (versions.packageJson !== versions.pomXml) {
350
- versions.mismatch = true;
351
- logger.warning(
352
- 'version-manager - getCurrentVersion',
353
- 'Version mismatch in monorepo',
354
- { packageJson: versions.packageJson, pomXml: versions.pomXml }
355
- );
356
- }
357
- }
358
-
359
- logger.debug('version-manager - getCurrentVersion', 'Versions retrieved', versions);
360
-
361
- return versions;
362
-
363
- } catch (error) {
364
- logger.error('version-manager - getCurrentVersion', 'Failed to get version', error);
365
- throw error;
366
- }
367
- }
368
-
369
700
  /**
370
701
  * Parses version string into components
371
702
  * Why: Enables version manipulation and comparison
@@ -452,129 +783,187 @@ export function incrementVersion(currentVersion, bumpType, suffix = null) {
452
783
  }
453
784
 
454
785
  /**
455
- * Updates version in package.json
456
- * Why: Writes new version to Node.js project file
786
+ * Updates version in build.gradle
787
+ * Why: Writes new version to Gradle project file
457
788
  *
789
+ * @param {string} filePath - Path to build.gradle
458
790
  * @param {string} newVersion - New version string
459
791
  */
460
- function updatePackageJsonVersion(newVersion) {
461
- logger.debug('version-manager - updatePackageJsonVersion', 'Updating package.json', { newVersion });
792
+ function updateGradleVersion(filePath, newVersion) {
793
+ logger.debug('version-manager - updateGradleVersion', 'Updating build.gradle', { filePath, newVersion });
462
794
 
463
795
  try {
464
- const packageJsonPath = discoveredPaths.packageJson;
465
-
466
- if (!packageJsonPath) {
467
- throw new Error('package.json path not discovered');
796
+ if (!fs.existsSync(filePath)) {
797
+ throw new Error(`build.gradle not found at ${filePath}`);
468
798
  }
469
799
 
470
- const content = fs.readFileSync(packageJsonPath, 'utf8');
471
- const packageData = JSON.parse(content);
800
+ const content = fs.readFileSync(filePath, 'utf8');
472
801
 
473
- packageData.version = newVersion;
802
+ // Replace: version = '...' or version '...' or version="..." or version "..."
803
+ const versionRegex = /(version\s*=?\s*)['"]([^'"]+)['"]/;
804
+ const newContent = content.replace(versionRegex, `$1'${newVersion}'`);
474
805
 
475
- // Write with 4-space indent to match existing format
476
- const updatedContent = `${JSON.stringify(packageData, null, 4)}\n`;
477
- fs.writeFileSync(packageJsonPath, updatedContent, 'utf8');
806
+ if (content === newContent) {
807
+ throw new Error('Could not find version property in build.gradle');
808
+ }
478
809
 
479
- logger.debug('version-manager - updatePackageJsonVersion', 'package.json updated', {
480
- path: packageJsonPath
481
- });
810
+ fs.writeFileSync(filePath, newContent, 'utf8');
811
+
812
+ logger.debug('version-manager - updateGradleVersion', 'build.gradle updated', { path: filePath });
482
813
 
483
814
  } catch (error) {
484
- logger.error('version-manager - updatePackageJsonVersion', 'Failed to update package.json', error);
485
- throw new Error(`Failed to update package.json: ${error.message}`);
815
+ logger.error('version-manager - updateGradleVersion', 'Failed to update build.gradle', error);
816
+ throw new Error(`Failed to update build.gradle: ${error.message}`);
486
817
  }
487
818
  }
488
819
 
489
820
  /**
490
- * Updates version in pom.xml
491
- * Why: Writes new version to Maven project file
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>
821
+ * Updates version in build.gradle.kts (Kotlin DSL)
822
+ * Why: Writes new version to Gradle Kotlin DSL project file
496
823
  *
824
+ * @param {string} filePath - Path to build.gradle.kts
497
825
  * @param {string} newVersion - New version string
498
826
  */
499
- function updatePomXmlVersion(newVersion) {
500
- logger.debug('version-manager - updatePomXmlVersion', 'Updating pom.xml', { newVersion });
827
+ function updateGradleKtsVersion(filePath, newVersion) {
828
+ logger.debug('version-manager - updateGradleKtsVersion', 'Updating build.gradle.kts', { filePath, newVersion });
501
829
 
502
830
  try {
503
- const pomXmlPath = discoveredPaths.pomXml;
831
+ if (!fs.existsSync(filePath)) {
832
+ throw new Error(`build.gradle.kts not found at ${filePath}`);
833
+ }
834
+
835
+ const content = fs.readFileSync(filePath, 'utf8');
836
+
837
+ // Replace: version = "..."
838
+ const versionRegex = /(version\s*=\s*)"([^"]+)"/;
839
+ const newContent = content.replace(versionRegex, `$1"${newVersion}"`);
840
+
841
+ if (content === newContent) {
842
+ throw new Error('Could not find version property in build.gradle.kts');
843
+ }
504
844
 
505
- if (!pomXmlPath) {
506
- throw new Error('pom.xml path not discovered');
845
+ fs.writeFileSync(filePath, newContent, 'utf8');
846
+
847
+ logger.debug('version-manager - updateGradleKtsVersion', 'build.gradle.kts updated', { path: filePath });
848
+
849
+ } catch (error) {
850
+ logger.error('version-manager - updateGradleKtsVersion', 'Failed to update build.gradle.kts', error);
851
+ throw new Error(`Failed to update build.gradle.kts: ${error.message}`);
852
+ }
853
+ }
854
+
855
+ /**
856
+ * Updates version in pyproject.toml
857
+ * Why: Writes new version to Python project file
858
+ *
859
+ * @param {string} filePath - Path to pyproject.toml
860
+ * @param {string} newVersion - New version string
861
+ */
862
+ function updatePyprojectVersion(filePath, newVersion) {
863
+ logger.debug('version-manager - updatePyprojectVersion', 'Updating pyproject.toml', { filePath, newVersion });
864
+
865
+ try {
866
+ if (!fs.existsSync(filePath)) {
867
+ throw new Error(`pyproject.toml not found at ${filePath}`);
507
868
  }
508
869
 
509
- const content = fs.readFileSync(pomXmlPath, 'utf8');
870
+ const content = fs.readFileSync(filePath, 'utf8');
510
871
 
511
- // Check if <parent> block exists
512
- const hasParent = /<parent>[\s\S]*?<\/parent>/.test(content);
513
- let newContent;
872
+ // Try to replace in [project] section first
873
+ let newContent = content.replace(
874
+ /(\[project\][\s\S]*?version\s*=\s*)["']([^"']+)["']/,
875
+ `$1"${newVersion}"`
876
+ );
514
877
 
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`);
878
+ // If not found, try [tool.poetry] section
879
+ if (content === newContent) {
880
+ newContent = content.replace(
881
+ /(\[tool\.poetry\][\s\S]*?version\s*=\s*)["']([^"']+)["']/,
882
+ `$1"${newVersion}"`
883
+ );
524
884
  }
525
885
 
526
886
  if (content === newContent) {
527
- logger.warning('version-manager - updatePomXmlVersion', 'No version tag found to update');
528
- throw new Error('Could not find <version> tag in pom.xml');
887
+ throw new Error('Could not find version in pyproject.toml');
529
888
  }
530
889
 
531
- fs.writeFileSync(pomXmlPath, newContent, 'utf8');
890
+ fs.writeFileSync(filePath, newContent, 'utf8');
532
891
 
533
- logger.debug('version-manager - updatePomXmlVersion', 'pom.xml updated', {
534
- path: pomXmlPath,
535
- hasParent
536
- });
892
+ logger.debug('version-manager - updatePyprojectVersion', 'pyproject.toml updated', { path: filePath });
537
893
 
538
894
  } catch (error) {
539
- logger.error('version-manager - updatePomXmlVersion', 'Failed to update pom.xml', error);
540
- throw new Error(`Failed to update pom.xml: ${error.message}`);
895
+ logger.error('version-manager - updatePyprojectVersion', 'Failed to update pyproject.toml', error);
896
+ throw new Error(`Failed to update pyproject.toml: ${error.message}`);
541
897
  }
542
898
  }
543
899
 
544
900
  /**
545
- * Updates version in project files
546
- * Why: Applies new version to all relevant files atomically
901
+ * Updates version in Cargo.toml
902
+ * Why: Writes new version to Rust project file
547
903
  *
548
- * @param {string} projectType - Project type from detectProjectType()
904
+ * @param {string} filePath - Path to Cargo.toml
549
905
  * @param {string} newVersion - New version string
550
906
  */
551
- export function updateVersion(projectType, newVersion) {
552
- logger.debug('version-manager - updateVersion', 'Updating version in files', {
553
- projectType,
554
- newVersion
555
- });
907
+ function updateCargoVersion(filePath, newVersion) {
908
+ logger.debug('version-manager - updateCargoVersion', 'Updating Cargo.toml', { filePath, newVersion });
556
909
 
557
910
  try {
558
- // Validate version format first
559
- parseVersion(newVersion); // Throws if invalid
911
+ if (!fs.existsSync(filePath)) {
912
+ throw new Error(`Cargo.toml not found at ${filePath}`);
913
+ }
914
+
915
+ const content = fs.readFileSync(filePath, 'utf8');
560
916
 
561
- if (projectType === PROJECT_TYPES.NODE || projectType === PROJECT_TYPES.BOTH) {
562
- updatePackageJsonVersion(newVersion);
917
+ // Replace version in [package] section
918
+ const versionRegex = /(\[package\][\s\S]*?version\s*=\s*)["']([^"']+)["']/;
919
+ const newContent = content.replace(versionRegex, `$1"${newVersion}"`);
920
+
921
+ if (content === newContent) {
922
+ throw new Error('Could not find version in Cargo.toml [package] section');
563
923
  }
564
924
 
565
- if (projectType === PROJECT_TYPES.MAVEN || projectType === PROJECT_TYPES.BOTH) {
566
- updatePomXmlVersion(newVersion);
925
+ fs.writeFileSync(filePath, newContent, 'utf8');
926
+
927
+ logger.debug('version-manager - updateCargoVersion', 'Cargo.toml updated', { path: filePath });
928
+
929
+ } catch (error) {
930
+ logger.error('version-manager - updateCargoVersion', 'Failed to update Cargo.toml', error);
931
+ throw new Error(`Failed to update Cargo.toml: ${error.message}`);
932
+ }
933
+ }
934
+
935
+ /**
936
+ * Updates version in version.sbt
937
+ * Why: Writes new version to Scala/sbt project file
938
+ *
939
+ * @param {string} filePath - Path to version.sbt
940
+ * @param {string} newVersion - New version string
941
+ */
942
+ function updateSbtVersion(filePath, newVersion) {
943
+ logger.debug('version-manager - updateSbtVersion', 'Updating version.sbt', { filePath, newVersion });
944
+
945
+ try {
946
+ if (!fs.existsSync(filePath)) {
947
+ throw new Error(`version.sbt not found at ${filePath}`);
567
948
  }
568
949
 
569
- if (projectType === PROJECT_TYPES.NONE) {
570
- throw new Error('No version files found (package.json or pom.xml)');
950
+ const content = fs.readFileSync(filePath, 'utf8');
951
+
952
+ // Replace: ThisBuild / version := "..." or version := "..."
953
+ const versionRegex = /((?:ThisBuild\s*\/\s*)?version\s*:=\s*)["']([^"']+)["']/;
954
+ const newContent = content.replace(versionRegex, `$1"${newVersion}"`);
955
+
956
+ if (content === newContent) {
957
+ throw new Error('Could not find version in version.sbt');
571
958
  }
572
959
 
573
- logger.debug('version-manager - updateVersion', 'Version updated successfully');
960
+ fs.writeFileSync(filePath, newContent, 'utf8');
961
+
962
+ logger.debug('version-manager - updateSbtVersion', 'version.sbt updated', { path: filePath });
574
963
 
575
964
  } catch (error) {
576
- logger.error('version-manager - updateVersion', 'Failed to update version', error);
577
- throw error;
965
+ logger.error('version-manager - updateSbtVersion', 'Failed to update version.sbt', error);
966
+ throw new Error(`Failed to update version.sbt: ${error.message}`);
578
967
  }
579
968
  }
580
969
 
@@ -661,8 +1050,8 @@ export async function validateVersionAlignment() {
661
1050
  logger.debug('version-manager - validateVersionAlignment', 'Validating version alignment');
662
1051
 
663
1052
  try {
664
- const projectType = detectProjectType();
665
- const versions = getCurrentVersion(projectType);
1053
+ // Discover all version files
1054
+ const discovery = discoverVersionFiles();
666
1055
 
667
1056
  // Get git tag version (parseTagVersion returns null for non-semver tags)
668
1057
  const { getLatestLocalTag, parseTagVersion } = await import('./git-tag-manager.js');
@@ -681,14 +1070,14 @@ export async function validateVersionAlignment() {
681
1070
  const localVersions = [];
682
1071
  const issues = [];
683
1072
 
684
- if (versions.packageJson) {
685
- localVersions.push(versions.packageJson);
686
- issues.push({ source: 'package.json', version: versions.packageJson });
687
- }
688
- if (versions.pomXml) {
689
- localVersions.push(versions.pomXml);
690
- issues.push({ source: 'pom.xml', version: versions.pomXml });
1073
+ // Add versions from discovered files
1074
+ for (const file of discovery.files) {
1075
+ if (file.version) {
1076
+ localVersions.push(file.version);
1077
+ issues.push({ source: file.relativePath, version: file.version });
1078
+ }
691
1079
  }
1080
+
692
1081
  if (changelogVersion) {
693
1082
  localVersions.push(changelogVersion);
694
1083
  issues.push({ source: 'CHANGELOG.md', version: changelogVersion });
@@ -739,12 +1128,3 @@ export async function validateVersionAlignment() {
739
1128
  }
740
1129
  }
741
1130
 
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
- }