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.
- package/CHANGELOG.md +116 -5
- package/LICENSE +20 -20
- package/bin/claude-hooks +84 -84
- package/lib/commands/analyze-diff.js +0 -1
- package/lib/commands/bump-version.js +346 -114
- package/lib/commands/create-pr.js +1 -1
- package/lib/commands/debug.js +1 -1
- package/lib/commands/generate-changelog.js +5 -6
- package/lib/commands/helpers.js +11 -6
- package/lib/commands/install.js +7 -8
- package/lib/commands/migrate-config.js +24 -2
- package/lib/commands/setup-github.js +1 -1
- package/lib/commands/update.js +1 -1
- package/lib/config.js +0 -4
- package/lib/hooks/prepare-commit-msg.js +2 -2
- package/lib/utils/changelog-generator.js +6 -8
- package/lib/utils/claude-client.js +7 -6
- package/lib/utils/claude-diagnostics.js +14 -21
- package/lib/utils/file-operations.js +1 -1
- package/lib/utils/file-utils.js +0 -1
- package/lib/utils/git-operations.js +0 -1
- package/lib/utils/github-api.js +3 -3
- package/lib/utils/github-client.js +2 -2
- package/lib/utils/installation-diagnostics.js +1 -1
- package/lib/utils/interactive-ui.js +93 -0
- package/lib/utils/prompt-builder.js +4 -6
- package/lib/utils/sanitize.js +13 -14
- package/lib/utils/task-id.js +18 -20
- package/lib/utils/telemetry.js +5 -7
- package/lib/utils/version-manager.js +676 -296
- package/package.json +68 -60
- package/templates/config.advanced.example.json +113 -113
- package/templates/pre-commit +7 -0
- package/templates/presets/ai/config.json +5 -5
- package/templates/presets/backend/config.json +5 -5
- package/templates/presets/backend/preset.json +49 -49
- package/templates/presets/database/config.json +5 -5
- package/templates/presets/database/preset.json +38 -38
- package/templates/presets/default/config.json +5 -5
- package/templates/presets/default/preset.json +53 -53
- package/templates/presets/frontend/config.json +5 -5
- package/templates/presets/frontend/preset.json +50 -50
- package/templates/presets/fullstack/config.json +5 -5
- 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
|
-
*
|
|
19
|
+
* Cache for discovery result
|
|
20
|
+
* Why: Store full discovery result for access by getDiscoveryResult()
|
|
20
21
|
*/
|
|
21
|
-
|
|
22
|
-
NODE: 'node',
|
|
23
|
-
MAVEN: 'maven',
|
|
24
|
-
BOTH: 'both',
|
|
25
|
-
NONE: 'none'
|
|
26
|
-
};
|
|
22
|
+
let cachedDiscoveryResult = null;
|
|
27
23
|
|
|
28
24
|
/**
|
|
29
|
-
*
|
|
30
|
-
* Why:
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
39
|
-
* Why: Support monorepos with version files
|
|
137
|
+
* Discovers all version files recursively in repository
|
|
138
|
+
* Why: Support monorepos with multiple version files of same/different types
|
|
40
139
|
*
|
|
41
|
-
* @
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
const rootPomXml = path.join(repoRoot, 'pom.xml');
|
|
189
|
+
try {
|
|
190
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
52
191
|
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
444
|
+
return newVersion;
|
|
120
445
|
}
|
|
121
446
|
|
|
122
447
|
/**
|
|
123
|
-
*
|
|
124
|
-
* Why:
|
|
448
|
+
* Reads current version from build.gradle
|
|
449
|
+
* Why: Gradle projects store version in version = '...' or version '...'
|
|
125
450
|
*
|
|
126
|
-
* @
|
|
451
|
+
* @param {string} filePath - Path to build.gradle
|
|
452
|
+
* @returns {string|null} Version string or null if not found
|
|
127
453
|
*/
|
|
128
|
-
|
|
129
|
-
logger.debug('version-manager -
|
|
454
|
+
function readGradleVersion(filePath) {
|
|
455
|
+
logger.debug('version-manager - readGradleVersion', 'Reading version from build.gradle', { filePath });
|
|
130
456
|
|
|
131
457
|
try {
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
136
|
-
const
|
|
465
|
+
// Match: version = '...' or version '...' or version="..." or version "..."
|
|
466
|
+
const versionRegex = /version\s*=?\s*['"]([^'"]+)['"]/;
|
|
467
|
+
const match = content.match(versionRegex);
|
|
137
468
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
150
|
-
logger.debug('version-manager -
|
|
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
|
|
477
|
+
return version;
|
|
157
478
|
|
|
158
479
|
} catch (error) {
|
|
159
|
-
logger.error('version-manager -
|
|
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
|
|
166
|
-
* Why:
|
|
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
|
|
171
|
-
logger.debug('version-manager -
|
|
492
|
+
function readGradleKtsVersion(filePath) {
|
|
493
|
+
logger.debug('version-manager - readGradleKtsVersion', 'Reading version from build.gradle.kts', { filePath });
|
|
172
494
|
|
|
173
495
|
try {
|
|
174
|
-
|
|
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(
|
|
182
|
-
const packageData = JSON.parse(content);
|
|
183
|
-
const version = packageData.version || null;
|
|
501
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
184
502
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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 -
|
|
194
|
-
throw new Error(`Failed to read
|
|
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
|
|
200
|
-
* Why:
|
|
524
|
+
* Reads current version from pyproject.toml
|
|
525
|
+
* Why: Python projects use pyproject.toml with [project] or [tool.poetry] section
|
|
201
526
|
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
|
|
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
|
|
209
|
-
logger.debug('version-manager -
|
|
574
|
+
function readCargoVersion(filePath) {
|
|
575
|
+
logger.debug('version-manager - readCargoVersion', 'Reading version from Cargo.toml', { filePath });
|
|
210
576
|
|
|
211
577
|
try {
|
|
212
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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
|
|
594
|
+
const version = match[1].trim();
|
|
595
|
+
logger.debug('version-manager - readCargoVersion', 'Version read', { version, path: filePath });
|
|
220
596
|
|
|
221
|
-
|
|
222
|
-
const hasParent = /<parent>[\s\S]*?<\/parent>/.test(content);
|
|
223
|
-
let version = null;
|
|
597
|
+
return version;
|
|
224
598
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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
|
-
|
|
248
|
-
|
|
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 -
|
|
257
|
-
throw new Error(`Failed to read
|
|
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
|
|
456
|
-
* Why: Writes new version to
|
|
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
|
|
461
|
-
logger.debug('version-manager -
|
|
792
|
+
function updateGradleVersion(filePath, newVersion) {
|
|
793
|
+
logger.debug('version-manager - updateGradleVersion', 'Updating build.gradle', { filePath, newVersion });
|
|
462
794
|
|
|
463
795
|
try {
|
|
464
|
-
|
|
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(
|
|
471
|
-
const packageData = JSON.parse(content);
|
|
800
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
472
801
|
|
|
473
|
-
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
806
|
+
if (content === newContent) {
|
|
807
|
+
throw new Error('Could not find version property in build.gradle');
|
|
808
|
+
}
|
|
478
809
|
|
|
479
|
-
|
|
480
|
-
|
|
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 -
|
|
485
|
-
throw new Error(`Failed to update
|
|
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
|
|
491
|
-
* Why: Writes new version to
|
|
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
|
|
500
|
-
logger.debug('version-manager -
|
|
827
|
+
function updateGradleKtsVersion(filePath, newVersion) {
|
|
828
|
+
logger.debug('version-manager - updateGradleKtsVersion', 'Updating build.gradle.kts', { filePath, newVersion });
|
|
501
829
|
|
|
502
830
|
try {
|
|
503
|
-
|
|
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
|
-
|
|
506
|
-
|
|
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(
|
|
870
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
510
871
|
|
|
511
|
-
//
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
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(
|
|
890
|
+
fs.writeFileSync(filePath, newContent, 'utf8');
|
|
532
891
|
|
|
533
|
-
logger.debug('version-manager -
|
|
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 -
|
|
540
|
-
throw new Error(`Failed to update
|
|
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
|
|
546
|
-
* Why:
|
|
901
|
+
* Updates version in Cargo.toml
|
|
902
|
+
* Why: Writes new version to Rust project file
|
|
547
903
|
*
|
|
548
|
-
* @param {string}
|
|
904
|
+
* @param {string} filePath - Path to Cargo.toml
|
|
549
905
|
* @param {string} newVersion - New version string
|
|
550
906
|
*/
|
|
551
|
-
|
|
552
|
-
logger.debug('version-manager -
|
|
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
|
-
|
|
559
|
-
|
|
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
|
-
|
|
562
|
-
|
|
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
|
-
|
|
566
|
-
|
|
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
|
-
|
|
570
|
-
|
|
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
|
-
|
|
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 -
|
|
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
|
-
|
|
665
|
-
const
|
|
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
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
-
}
|