claude-git-hooks 2.51.2 → 2.66.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 +513 -30
- package/lib/commands/back-merge.js +91 -11
- package/lib/commands/bump-version.js +72 -76
- package/lib/commands/create-pr.js +124 -44
- package/lib/commands/create-release.js +198 -52
- package/lib/commands/help.js +13 -3
- package/lib/messages/library-warnings.js +147 -0
- package/lib/utils/claude-client.js +6 -1
- package/lib/utils/git-tag-manager.js +104 -0
- package/lib/utils/github-api.js +30 -0
- package/lib/utils/judge.js +2 -1
- package/lib/utils/linter-runner.js +6 -0
- package/lib/utils/version-manager.js +30 -17
- package/package.json +85 -83
|
@@ -196,6 +196,51 @@ export function getLatestLocalTag() {
|
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Gets latest local tag reachable from HEAD
|
|
201
|
+
* Why: Excludes rogue tags from unrelated branches that were fetched locally
|
|
202
|
+
*
|
|
203
|
+
* @returns {string|null} Latest semver tag reachable from HEAD, or null
|
|
204
|
+
*/
|
|
205
|
+
export function getLatestLocalTagOnBranch() {
|
|
206
|
+
logger.debug('git-tag-manager - getLatestLocalTagOnBranch', 'Getting latest local tag on HEAD');
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const output = execGitTagCommand('git tag --merged HEAD --sort=-v:refname');
|
|
210
|
+
|
|
211
|
+
if (!output) {
|
|
212
|
+
logger.debug('git-tag-manager - getLatestLocalTagOnBranch', 'No tags on HEAD');
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const tags = output.split(/\r?\n/).filter((t) => t.length > 0);
|
|
217
|
+
const semverTags = tags.filter(isSemverTag);
|
|
218
|
+
|
|
219
|
+
if (semverTags.length === 0) {
|
|
220
|
+
logger.debug('git-tag-manager - getLatestLocalTagOnBranch', 'No semver tags on HEAD', {
|
|
221
|
+
totalTags: tags.length
|
|
222
|
+
});
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const latestTag = semverTags[0];
|
|
227
|
+
|
|
228
|
+
logger.debug('git-tag-manager - getLatestLocalTagOnBranch', 'Latest tag on HEAD', {
|
|
229
|
+
latestTag,
|
|
230
|
+
semverTags: semverTags.length
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
return latestTag;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
logger.error(
|
|
236
|
+
'git-tag-manager - getLatestLocalTagOnBranch',
|
|
237
|
+
'Failed to get tags on HEAD',
|
|
238
|
+
error
|
|
239
|
+
);
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
199
244
|
/**
|
|
200
245
|
* Gets all remote tags
|
|
201
246
|
* Why: Compare local tags with remote for push status
|
|
@@ -311,6 +356,65 @@ export async function getLatestRemoteTag(remoteName = null) {
|
|
|
311
356
|
}
|
|
312
357
|
}
|
|
313
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Gets latest remote tag reachable from a specific branch
|
|
361
|
+
* Why: Scoped comparison — avoids rogue tags pushed from unmerged feature branches
|
|
362
|
+
*
|
|
363
|
+
* @param {string} baseBranch - Branch to scope tags to (e.g., 'develop')
|
|
364
|
+
* @param {string} remoteName - Remote name (default: 'origin')
|
|
365
|
+
* @returns {string|null} Latest semver tag name reachable from the branch, or null
|
|
366
|
+
*/
|
|
367
|
+
export function getLatestRemoteTagOnBranch(baseBranch, remoteName = null) {
|
|
368
|
+
const remote = remoteName || getRemoteName();
|
|
369
|
+
logger.debug('git-tag-manager - getLatestRemoteTagOnBranch', 'Getting latest tag on branch', {
|
|
370
|
+
baseBranch,
|
|
371
|
+
remote
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
// Ensure remote branch ref is fresh
|
|
376
|
+
execGitTagCommand(`git fetch ${remote} ${baseBranch} --quiet`);
|
|
377
|
+
|
|
378
|
+
// Get tags merged into the remote branch, sorted by version descending
|
|
379
|
+
const output = execGitTagCommand(
|
|
380
|
+
`git tag --merged ${remote}/${baseBranch} --sort=-v:refname`
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
if (!output) {
|
|
384
|
+
logger.debug('git-tag-manager - getLatestRemoteTagOnBranch', 'No tags on branch');
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const tags = output.split(/\r?\n/).filter((line) => line.length > 0);
|
|
389
|
+
const semverTags = tags.filter(isSemverTag);
|
|
390
|
+
|
|
391
|
+
if (semverTags.length === 0) {
|
|
392
|
+
logger.debug('git-tag-manager - getLatestRemoteTagOnBranch', 'No semver tags on branch', {
|
|
393
|
+
totalTags: tags.length
|
|
394
|
+
});
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Already sorted by git --sort=-v:refname (descending), first is latest
|
|
399
|
+
const latestTag = semverTags[0];
|
|
400
|
+
|
|
401
|
+
logger.debug('git-tag-manager - getLatestRemoteTagOnBranch', 'Latest tag on branch', {
|
|
402
|
+
baseBranch,
|
|
403
|
+
latestTag,
|
|
404
|
+
semverTags: semverTags.length
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
return latestTag;
|
|
408
|
+
} catch (error) {
|
|
409
|
+
logger.error(
|
|
410
|
+
'git-tag-manager - getLatestRemoteTagOnBranch',
|
|
411
|
+
'Failed to get tags on branch',
|
|
412
|
+
error
|
|
413
|
+
);
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
314
418
|
/**
|
|
315
419
|
* Checks if tag exists
|
|
316
420
|
* Why: Prevents duplicate tag creation
|
package/lib/utils/github-api.js
CHANGED
|
@@ -940,6 +940,36 @@ export const findExistingPR = async ({ owner, repo, head, base }) => {
|
|
|
940
940
|
}
|
|
941
941
|
};
|
|
942
942
|
|
|
943
|
+
/**
|
|
944
|
+
* Update the body of an existing pull request
|
|
945
|
+
* @param {string} owner - Repository owner
|
|
946
|
+
* @param {string} repo - Repository name
|
|
947
|
+
* @param {number} number - PR number
|
|
948
|
+
* @param {string} body - New PR body content
|
|
949
|
+
* @returns {Promise<Object>} Updated PR data
|
|
950
|
+
*/
|
|
951
|
+
export const updatePullRequestBody = async (owner, repo, number, body) => {
|
|
952
|
+
logger.debug('github-api - updatePullRequestBody', 'Updating PR body', {
|
|
953
|
+
owner, repo, number, bodyLength: body.length
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
const octokit = getOctokit();
|
|
957
|
+
|
|
958
|
+
const { data } = await octokit.pulls.update({
|
|
959
|
+
owner,
|
|
960
|
+
repo,
|
|
961
|
+
pull_number: number,
|
|
962
|
+
body
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
logger.debug('github-api - updatePullRequestBody', 'PR body updated', {
|
|
966
|
+
number: data.number,
|
|
967
|
+
url: data.html_url
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
return data;
|
|
971
|
+
};
|
|
972
|
+
|
|
943
973
|
/**
|
|
944
974
|
* Get repository information
|
|
945
975
|
* Why: Fetch repo metadata for validation and context
|
package/lib/utils/judge.js
CHANGED
|
@@ -139,7 +139,8 @@ const judgeAndFix = async (analysisResult, filesData, config, { headless = false
|
|
|
139
139
|
const response = await executeClaudeWithRetry(prompt, {
|
|
140
140
|
model,
|
|
141
141
|
timeout: judgeTimeout,
|
|
142
|
-
headless
|
|
142
|
+
headless,
|
|
143
|
+
print: true
|
|
143
144
|
});
|
|
144
145
|
|
|
145
146
|
const parsed = extractJSON(response);
|
|
@@ -45,6 +45,12 @@ export function parseEslintOutput(stdout) {
|
|
|
45
45
|
|
|
46
46
|
for (const fileResult of results) {
|
|
47
47
|
for (const msg of fileResult.messages || []) {
|
|
48
|
+
// Skip ESLint meta-warnings about ignored files — not code quality issues.
|
|
49
|
+
// Dot-directory files (e.g. .library/) trigger "File ignored by default"
|
|
50
|
+
// even when ignorePatterns negation is set, because ESLint's default
|
|
51
|
+
// dot-directory ignore takes precedence for explicitly passed file paths.
|
|
52
|
+
if (msg.message && msg.message.startsWith('File ignored')) continue;
|
|
53
|
+
|
|
48
54
|
const issue = {
|
|
49
55
|
file: fileResult.filePath || '',
|
|
50
56
|
line: msg.line,
|
|
@@ -248,13 +248,14 @@ export function discoverVersionFiles(options = {}) {
|
|
|
248
248
|
const registry = VERSION_FILE_TYPES[fileType];
|
|
249
249
|
if (registry && entry.name === registry.filename) {
|
|
250
250
|
const version = registry.readVersion(fullPath);
|
|
251
|
+
const isSemver = version !== null && validateVersionFormat(version);
|
|
251
252
|
const descriptor = {
|
|
252
253
|
path: fullPath,
|
|
253
254
|
relativePath: path.relative(repoRoot, fullPath),
|
|
254
255
|
type: fileType,
|
|
255
256
|
projectLabel: registry.projectLabel,
|
|
256
257
|
version,
|
|
257
|
-
selected:
|
|
258
|
+
selected: isSemver
|
|
258
259
|
};
|
|
259
260
|
discoveredFiles.push(descriptor);
|
|
260
261
|
logger.debug('version-manager - discoverVersionFiles', 'Found version file', {
|
|
@@ -285,19 +286,19 @@ export function discoverVersionFiles(options = {}) {
|
|
|
285
286
|
return a.relativePath.localeCompare(b.relativePath);
|
|
286
287
|
});
|
|
287
288
|
|
|
288
|
-
// Determine resolved version (prefer root-level
|
|
289
|
+
// Determine resolved version from semver files only (prefer root-level, then first found)
|
|
289
290
|
let resolvedVersion = null;
|
|
290
|
-
const
|
|
291
|
+
const semverFiles = discoveredFiles.filter((f) => f.selected);
|
|
292
|
+
const rootFile = semverFiles.find((f) => !f.relativePath.includes(path.sep));
|
|
291
293
|
if (rootFile && rootFile.version) {
|
|
292
294
|
resolvedVersion = rootFile.version;
|
|
293
|
-
} else if (
|
|
294
|
-
|
|
295
|
-
const firstWithVersion = discoveredFiles.find((f) => f.version !== null);
|
|
295
|
+
} else if (semverFiles.length > 0) {
|
|
296
|
+
const firstWithVersion = semverFiles.find((f) => f.version !== null);
|
|
296
297
|
resolvedVersion = firstWithVersion ? firstWithVersion.version : null;
|
|
297
298
|
}
|
|
298
299
|
|
|
299
|
-
// Check for version mismatch
|
|
300
|
-
const versions =
|
|
300
|
+
// Check for version mismatch (only among semver files)
|
|
301
|
+
const versions = semverFiles.filter((f) => f.version !== null).map((f) => f.version);
|
|
301
302
|
const uniqueVersions = [...new Set(versions)];
|
|
302
303
|
const mismatch = uniqueVersions.length > 1;
|
|
303
304
|
|
|
@@ -1149,24 +1150,36 @@ export function compareVersions(version1, version2) {
|
|
|
1149
1150
|
*
|
|
1150
1151
|
* @returns {Promise<Object>} Validation result with alignment status and issues
|
|
1151
1152
|
*/
|
|
1152
|
-
export async function validateVersionAlignment() {
|
|
1153
|
-
logger.debug('version-manager - validateVersionAlignment', 'Validating version alignment'
|
|
1153
|
+
export async function validateVersionAlignment(baseBranch = null) {
|
|
1154
|
+
logger.debug('version-manager - validateVersionAlignment', 'Validating version alignment', {
|
|
1155
|
+
baseBranch
|
|
1156
|
+
});
|
|
1154
1157
|
|
|
1155
1158
|
try {
|
|
1156
1159
|
// Discover all version files
|
|
1157
1160
|
const discovery = discoverVersionFiles();
|
|
1158
1161
|
|
|
1159
|
-
// Get git tag version
|
|
1160
|
-
const {
|
|
1161
|
-
|
|
1162
|
+
// Get git tag version scoped to HEAD when baseBranch is provided
|
|
1163
|
+
const {
|
|
1164
|
+
getLatestLocalTag, getLatestLocalTagOnBranch,
|
|
1165
|
+
parseTagVersion,
|
|
1166
|
+
getLatestRemoteTag, getLatestRemoteTagOnBranch
|
|
1167
|
+
} = await import('./git-tag-manager.js');
|
|
1168
|
+
const latestTag = baseBranch ? getLatestLocalTagOnBranch(baseBranch) : getLatestLocalTag();
|
|
1162
1169
|
const tagVersion = latestTag ? parseTagVersion(latestTag) : null;
|
|
1163
1170
|
|
|
1164
1171
|
// Get CHANGELOG version
|
|
1165
1172
|
const changelogVersion = readChangelogVersion();
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1173
|
+
let latestRemoteTag;
|
|
1174
|
+
if (baseBranch) {
|
|
1175
|
+
latestRemoteTag = getLatestRemoteTagOnBranch(baseBranch);
|
|
1176
|
+
// Fall back to global if branch-scoped lookup returns nothing
|
|
1177
|
+
if (!latestRemoteTag) {
|
|
1178
|
+
latestRemoteTag = await getLatestRemoteTag();
|
|
1179
|
+
}
|
|
1180
|
+
} else {
|
|
1181
|
+
latestRemoteTag = await getLatestRemoteTag();
|
|
1182
|
+
}
|
|
1170
1183
|
const remoteVersion = latestRemoteTag ? parseTagVersion(latestRemoteTag) : null;
|
|
1171
1184
|
|
|
1172
1185
|
// Collect all local versions
|
package/package.json
CHANGED
|
@@ -1,83 +1,85 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "claude-git-hooks",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Git hooks with Claude CLI for code analysis and automatic commit messages",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"claude-hooks": "./bin/claude-hooks"
|
|
8
|
-
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"test": "npm run test:all",
|
|
11
|
-
"test:all": "npm run lint && npm run test:smoke && npm run test:unit && npm run test:integration",
|
|
12
|
-
"test:smoke": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/smoke --maxWorkers=1",
|
|
13
|
-
"test:unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --forceExit",
|
|
14
|
-
"test:integration": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration --maxWorkers=1 --testTimeout=30000 --forceExit",
|
|
15
|
-
"test:integration:ci": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration/ci-safe.test.js --maxWorkers=1 --testTimeout=30000 --forceExit",
|
|
16
|
-
"test:changed": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --changedSince=main --forceExit",
|
|
17
|
-
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
|
|
18
|
-
"test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"library:
|
|
27
|
-
"library:
|
|
28
|
-
"library:
|
|
29
|
-
"library:
|
|
30
|
-
"library:
|
|
31
|
-
"library:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
"jest": "^29.
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
|
|
83
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-git-hooks",
|
|
3
|
+
"version": "2.66.1",
|
|
4
|
+
"description": "Git hooks with Claude CLI for code analysis and automatic commit messages",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"claude-hooks": "./bin/claude-hooks"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "npm run test:all",
|
|
11
|
+
"test:all": "npm run lint && npm run test:smoke && npm run test:unit && npm run test:integration",
|
|
12
|
+
"test:smoke": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/smoke --maxWorkers=1",
|
|
13
|
+
"test:unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --forceExit",
|
|
14
|
+
"test:integration": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration --maxWorkers=1 --testTimeout=30000 --forceExit",
|
|
15
|
+
"test:integration:ci": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration/ci-safe.test.js --maxWorkers=1 --testTimeout=30000 --forceExit",
|
|
16
|
+
"test:changed": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --changedSince=main --forceExit",
|
|
17
|
+
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
|
|
18
|
+
"test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
|
|
19
|
+
"test:e2e": "bash test/manual/sdlc-stability-check.sh",
|
|
20
|
+
"test:full": "npm run test:all && npm run test:e2e",
|
|
21
|
+
"lint": "eslint lib/ bin/claude-hooks .library/librarian/",
|
|
22
|
+
"lint:fix": "eslint lib/ bin/claude-hooks .library/librarian/ --fix",
|
|
23
|
+
"format": "prettier --write \"lib/**/*.js\" \"bin/**\" \"test/**/*.js\"",
|
|
24
|
+
"precommit": "npm run lint && npm run test:smoke",
|
|
25
|
+
"prepublishOnly": "npm run test:all",
|
|
26
|
+
"library:check": "node .library/bin/library check",
|
|
27
|
+
"library:regenerate": "node .library/bin/library regenerate",
|
|
28
|
+
"library:extract": "node .library/bin/library extract",
|
|
29
|
+
"library:tokens": "node .library/bin/library tokens",
|
|
30
|
+
"library:graph": "node .library/bin/library graph",
|
|
31
|
+
"library:inject": "node .library/bin/library inject",
|
|
32
|
+
"library:validate": "node .library/bin/library validate",
|
|
33
|
+
"library:report": "node .library/bin/library report"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"git",
|
|
37
|
+
"hooks",
|
|
38
|
+
"claude",
|
|
39
|
+
"ai",
|
|
40
|
+
"code-review",
|
|
41
|
+
"commit-messages",
|
|
42
|
+
"pre-commit",
|
|
43
|
+
"automation"
|
|
44
|
+
],
|
|
45
|
+
"author": "Pablo Rovito",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "https://github.com/mscope-S-L/git-hooks.git"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=16.9.0"
|
|
53
|
+
},
|
|
54
|
+
"engineStrict": false,
|
|
55
|
+
"os": [
|
|
56
|
+
"darwin",
|
|
57
|
+
"linux",
|
|
58
|
+
"win32"
|
|
59
|
+
],
|
|
60
|
+
"preferGlobal": true,
|
|
61
|
+
"files": [
|
|
62
|
+
"bin/",
|
|
63
|
+
"lib/",
|
|
64
|
+
"templates/",
|
|
65
|
+
"README.md",
|
|
66
|
+
"CHANGELOG.md",
|
|
67
|
+
"CLAUDE.md",
|
|
68
|
+
"LICENSE"
|
|
69
|
+
],
|
|
70
|
+
"dependencies": {
|
|
71
|
+
"@anthropic-ai/sdk": "^0.91.0",
|
|
72
|
+
"@octokit/rest": "^21.0.0",
|
|
73
|
+
"langfuse": "^3.38.20"
|
|
74
|
+
},
|
|
75
|
+
"devDependencies": {
|
|
76
|
+
"@types/jest": "^29.5.0",
|
|
77
|
+
"eslint": "^8.57.1",
|
|
78
|
+
"jest": "^29.7.0",
|
|
79
|
+
"js-tiktoken": "^1.0.18",
|
|
80
|
+
"madge": "^8.0.0",
|
|
81
|
+
"prettier": "^3.2.0",
|
|
82
|
+
"tree-sitter-wasms": "^0.1.13",
|
|
83
|
+
"web-tree-sitter": "^0.24.7"
|
|
84
|
+
}
|
|
85
|
+
}
|