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.
- package/CHANGELOG.md +54 -1
- package/README.md +52 -13
- package/bin/claude-hooks +8 -0
- package/lib/commands/analyze-diff.js +32 -153
- package/lib/commands/analyze.js +217 -0
- package/lib/commands/bump-version.js +172 -50
- package/lib/commands/create-pr.js +15 -80
- package/lib/commands/helpers.js +7 -0
- package/lib/hooks/pre-commit.js +26 -265
- package/lib/utils/analysis-engine.js +469 -0
- package/lib/utils/claude-client.js +6 -1
- package/lib/utils/claude-diagnostics.js +2 -1
- package/lib/utils/git-operations.js +537 -1
- package/lib/utils/git-tag-manager.js +58 -8
- package/lib/utils/interactive-ui.js +86 -1
- package/lib/utils/pr-metadata-engine.js +474 -0
- package/lib/utils/resolution-prompt.js +57 -34
- package/lib/utils/version-manager.js +219 -52
- package/package.json +1 -1
|
@@ -35,60 +35,65 @@ class ResolutionPromptError extends Error {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* Formats
|
|
38
|
+
* Formats issues for resolution prompt
|
|
39
39
|
* Why: Structures issues in readable markdown format for AI processing
|
|
40
40
|
*
|
|
41
|
-
* @param {Array<Object>}
|
|
41
|
+
* @param {Array<Object>} issues - Array of issues (blocking or detailed)
|
|
42
42
|
* @returns {string} Formatted markdown
|
|
43
43
|
*
|
|
44
|
-
*
|
|
45
|
-
* {
|
|
46
|
-
*
|
|
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 = (
|
|
48
|
+
const formatBlockingIssues = (issues) => {
|
|
54
49
|
logger.debug(
|
|
55
50
|
'resolution-prompt - formatBlockingIssues',
|
|
56
|
-
'Formatting
|
|
57
|
-
{ issueCount:
|
|
51
|
+
'Formatting issues',
|
|
52
|
+
{ issueCount: issues?.length || 0 }
|
|
58
53
|
);
|
|
59
54
|
|
|
60
|
-
if (!Array.isArray(
|
|
61
|
-
return 'No
|
|
55
|
+
if (!Array.isArray(issues) || issues.length === 0) {
|
|
56
|
+
return 'No issues found.';
|
|
62
57
|
}
|
|
63
58
|
|
|
64
|
-
return
|
|
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
|
-
|
|
70
|
-
**Description:** ${
|
|
71
|
-
**Location:** ${issue.file || 'unknown'}:${issue.line || '?'}
|
|
72
|
-
|
|
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
|
|
84
|
+
* Gets unique file paths from issues
|
|
80
85
|
* Why: Need to include full content of affected files
|
|
81
86
|
*
|
|
82
|
-
* @param {Array<Object>}
|
|
87
|
+
* @param {Array<Object>} issues - Array of issues (blocking or detailed)
|
|
83
88
|
* @returns {Array<string>} Unique file paths
|
|
84
89
|
*/
|
|
85
|
-
const getAffectedFiles = (
|
|
86
|
-
if (!Array.isArray(
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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
|
|
219
|
-
const issuesFormatted = formatBlockingIssues(
|
|
230
|
+
// Format all issues
|
|
231
|
+
const issuesFormatted = formatBlockingIssues(allIssues);
|
|
220
232
|
|
|
221
233
|
// Get and format affected files
|
|
222
|
-
const affectedFiles = getAffectedFiles(
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
const pomXmlPath = path.join(repoRoot, 'pom.xml');
|
|
132
|
+
// Discover version files (checks root and subdirectories)
|
|
133
|
+
discoveredPaths = discoverVersionFiles();
|
|
41
134
|
|
|
42
|
-
const hasPackageJson =
|
|
43
|
-
const hasPomXml =
|
|
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
|
-
|
|
59
|
-
|
|
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
|
|
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', {
|
|
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
|
|
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
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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 (!
|
|
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
|
-
|
|
133
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
|
347
|
-
|
|
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)
|
|
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
|
|
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
|
|
377
|
-
const pomXmlPath = path.join(repoRoot, 'pom.xml');
|
|
503
|
+
const pomXmlPath = discoveredPaths.pomXml;
|
|
378
504
|
|
|
379
|
-
|
|
505
|
+
if (!pomXmlPath) {
|
|
506
|
+
throw new Error('pom.xml path not discovered');
|
|
507
|
+
}
|
|
380
508
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|