claude-git-hooks 2.61.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 +241 -3
- package/lib/commands/bump-version.js +33 -76
- package/lib/commands/create-pr.js +123 -83
- package/lib/commands/help.js +13 -3
- package/lib/messages/library-warnings.js +128 -10
- package/lib/utils/claude-client.js +6 -1
- package/lib/utils/git-tag-manager.js +104 -0
- package/lib/utils/judge.js +2 -1
- package/lib/utils/version-manager.js +30 -17
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,45 +5,283 @@ Todos los cambios notables en este proyecto se documentarán en este archivo.
|
|
|
5
5
|
El formato está basado en [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
y este proyecto adhiere a [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.66.1] - 2026-06-10
|
|
9
|
+
|
|
10
|
+
### 🐛 Fixed
|
|
11
|
+
- Normalized path separators to forward slashes in staleness checker for cross-platform compatibility
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [2.66.0] - 2026-06-10
|
|
15
|
+
|
|
16
|
+
### ✨ Added
|
|
17
|
+
- Multi-language extractor registry with pluggable per-language architecture [AUT-3742]
|
|
18
|
+
- End-to-end SDLC stability check test suite
|
|
19
|
+
- Version alignment validation now scoped to base branch tags (#185)
|
|
20
|
+
- Extractor notes template for documenting per-language extractors
|
|
21
|
+
|
|
22
|
+
### 🔧 Changed
|
|
23
|
+
- Moved arrow-function extraction from monolithic extractor to pluggable extractor module [AUT-3742]
|
|
24
|
+
- Decoupled `lib/` from `.library/` static imports with ESLint enforcement rule [AUT-3742]
|
|
25
|
+
- Library maintenance pipeline now runs before tag push in `create-pr`, ensuring library books are included in tagged commits
|
|
26
|
+
- Simplified `bump-version` interactive file selection to direct toggle list, removing multi-choice menu
|
|
27
|
+
- Tags are force-repointed to HEAD after library commit so tagged version includes regenerated books
|
|
28
|
+
|
|
29
|
+
### 🐛 Fixed
|
|
30
|
+
- Fixed `create-pr` gotcha solicitation receiving empty file paths instead of actual book content (#190)
|
|
31
|
+
- Fixed `create-pr` tag comparison using global tag list instead of base branch scope
|
|
32
|
+
- Fixed `create-pr` library pipeline leaking host-repo paths into foreign repositories (#186)
|
|
33
|
+
- Fixed `bump-version` crashing on non-semver version files such as Maven `${revision}` placeholders (#187)
|
|
34
|
+
- Fixed version tag lookup scanning all branches instead of only the HEAD branch (#185)
|
|
35
|
+
- Fixed stale git worktree leftovers causing integration test failures
|
|
36
|
+
|
|
37
|
+
### 🗑️ Removed
|
|
38
|
+
- Removed per-file custom version entry option (`promptEditField`) from `bump-version` interactive selection
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
## [2.65.0] - 2026-06-08
|
|
42
|
+
|
|
43
|
+
### ✨ Added
|
|
44
|
+
- Multi-language extractor registry with pluggable per-language extractors (AUT-3742)
|
|
45
|
+
- Extractor interface contract and runtime validator for language extractor plugins (AUT-3742)
|
|
46
|
+
- JS extractor notes template for documenting new extractors (AUT-3742)
|
|
47
|
+
- Test harness with JS fixtures for extractor validation (AUT-3742)
|
|
48
|
+
- Version alignment now scoped to base branch tags instead of global tag lookup
|
|
49
|
+
|
|
50
|
+
### 🔧 Changed
|
|
51
|
+
- Arrow-function extraction moved from monolithic extract.js to pluggable JS extractor module (AUT-3742)
|
|
52
|
+
- Decoupled lib/ from .library/ static imports with ESLint enforcement — lib/ must use dynamic import() for Library integration (AUT-3742)
|
|
53
|
+
- Library maintenance pipeline in create-pr now runs before tag push so tags include regenerated books
|
|
54
|
+
- Unpushed tags are force-repointed to HEAD after library commit to include book changes
|
|
55
|
+
- Gotcha solicitation now receives full book content instead of just file paths
|
|
56
|
+
- bump-version interactive file selection simplified — goes directly to toggle list, non-semver files pre-deselected
|
|
57
|
+
|
|
58
|
+
### 🐛 Fixed
|
|
59
|
+
- Fixed create-pr library pipeline running incorrectly in foreign repos by deriving paths from resolver config (#186)
|
|
60
|
+
- Fixed bump-version crash when non-semver files (e.g., Maven `${revision}`) reached version parser (#187)
|
|
61
|
+
- Fixed create-pr comparing tags against global scope instead of base branch (#185, #189)
|
|
62
|
+
- Fixed create-pr using stale tag reference after library pipeline commit
|
|
63
|
+
- Fixed local tag lookup not scoped to HEAD branch, causing cross-branch version mismatches
|
|
64
|
+
|
|
65
|
+
### 🗑️ Removed
|
|
66
|
+
- Removed per-file custom version editing (option `e`) from bump-version interactive selection
|
|
67
|
+
- Removed `promptMenu` and `promptEditField` usage from bump-version flow
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
## [2.64.4] - 2026-06-08
|
|
71
|
+
|
|
72
|
+
### ✨ Added
|
|
73
|
+
- Added multi-language extractor registry with pluggable architecture and runtime interface validation (AUT-3742, #184)
|
|
74
|
+
- Added JS extractor as first pluggable extractor with dedicated test harness and fixtures (AUT-3742)
|
|
75
|
+
- Added extractor notes template for documenting per-language extractors (AUT-3742)
|
|
76
|
+
- Added branch-scoped tag lookup functions `getLatestLocalTagOnBranch()` and `getLatestRemoteTagOnBranch()` to git-tag-manager (#185)
|
|
77
|
+
- Added branch-scoped version alignment validation in `create-pr` (#185)
|
|
78
|
+
|
|
79
|
+
### 🔧 Changed
|
|
80
|
+
- Decoupled `lib/` from `.library/` static imports with ESLint restricted-imports guard (AUT-3742)
|
|
81
|
+
- Moved arrow-function extraction from monolithic extractor into pluggable JS extractor module (AUT-3742)
|
|
82
|
+
- Simplified `bump-version` interactive file selection — removed menu, goes directly to toggle list with non-semver files pre-deselected (#187)
|
|
83
|
+
- Library pipeline in `create-pr` now derives paths from resolver config, preventing git-hooks paths from leaking into foreign repos (#186)
|
|
84
|
+
- Reordered `create-pr` steps so library pipeline runs before tag push, then re-points unpushed tags to HEAD to include library commits
|
|
85
|
+
|
|
86
|
+
### 🐛 Fixed
|
|
87
|
+
- Fixed `bump-version` crash when non-semver files (e.g., Maven `${revision}`) were included in version resolution (#187)
|
|
88
|
+
- Fixed `create-pr` library pipeline using hardcoded paths that broke in foreign repositories (#186)
|
|
89
|
+
- Fixed version alignment and tag lookup returning results from unrelated branches instead of scoping to base branch (#185)
|
|
90
|
+
- Fixed `create-pr` tag becoming stale after library book regeneration by running the pipeline before tag push and re-pointing tags
|
|
91
|
+
|
|
92
|
+
### 🗑️ Removed
|
|
93
|
+
- Removed `promptMenu` and `promptEditField` options from `bump-version` interactive file selection (#187)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
## [2.64.3] - 2026-06-08
|
|
97
|
+
|
|
98
|
+
### ✨ Added
|
|
99
|
+
- Added multi-language extractor registry with pluggable interface, including JS extractor and notes template (AUT-3742)
|
|
100
|
+
- Added library maintenance pipeline step in create-pr that regenerates books before tag push
|
|
101
|
+
- Added branch-scoped tag lookup functions (`getLatestLocalTagOnBranch`, `getLatestRemoteTagOnBranch`) to git-tag-manager
|
|
102
|
+
- Added ESLint rule preventing `lib/` from statically importing `.library/` modules (AUT-3742)
|
|
103
|
+
- Added extractor test harness with JS fixtures (AUT-3742)
|
|
104
|
+
|
|
105
|
+
### 🔧 Changed
|
|
106
|
+
- Moved arrow-function extraction from monolithic `extract.js` into pluggable extractor under `librarian/extractors/registered/js/` (AUT-3742)
|
|
107
|
+
- Scoped version alignment validation to base branch tags instead of global tag lookup
|
|
108
|
+
- Simplified bump-version interactive file selection — goes directly to toggle list, non-semver files pre-deselected
|
|
109
|
+
- Library pipeline now derives booksDir/sourceDir from resolver config when repoRoot is overridden, preventing path leakage into foreign repos
|
|
110
|
+
- Unpushed tags are force-repointed to HEAD after library commit so tags include regenerated books
|
|
111
|
+
|
|
112
|
+
### 🐛 Fixed
|
|
113
|
+
- Fixed create-pr using stale tag for comparison after library regeneration created a new commit
|
|
114
|
+
- Fixed create-pr running library pipeline in foreign repos where `.library/` paths were invalid (#186)
|
|
115
|
+
- Fixed bump-version crashing on non-semver version files (e.g., Maven `${revision}`) by excluding them from version resolution (#187)
|
|
116
|
+
- Fixed local tag lookup scanning tags from all branches instead of only the current HEAD branch (#185)
|
|
117
|
+
|
|
118
|
+
### 🗑️ Removed
|
|
119
|
+
- Removed per-file custom version editing (`promptEditField` option) from bump-version interactive selection
|
|
120
|
+
- Removed `promptMenu` four-option flow from bump-version in favor of direct toggle list
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
## [2.64.2] - 2026-06-08
|
|
124
|
+
|
|
125
|
+
### ✨ Added
|
|
126
|
+
- Multi-language extractor registry with pluggable extractor interface and JS extractor (AUT-3742, #184)
|
|
127
|
+
- Extractor notes template and per-extractor documentation standard (AUT-3742)
|
|
128
|
+
- Test harness for extractors with JS fixtures (AUT-3742)
|
|
129
|
+
- ESLint rule preventing lib/ from statically importing .library/ modules (AUT-3742)
|
|
130
|
+
- Version alignment now scoped to base branch tags for accurate cross-branch comparisons (#185)
|
|
131
|
+
- Library maintenance pipeline step in create-pr to regenerate books before tagging (#187)
|
|
132
|
+
|
|
133
|
+
### 🔧 Changed
|
|
134
|
+
- Moved arrow-function extraction from monolithic extractor to pluggable JS extractor module (AUT-3742)
|
|
135
|
+
- Decoupled lib/ from .library/ imports — library integration now uses dynamic import with graceful degradation (AUT-3742)
|
|
136
|
+
- Library pipeline in create-pr derives paths from resolver.yaml config instead of hardcoded values (#186)
|
|
137
|
+
- Simplified bump-version interactive file selection to use toggle list directly, removing menu options (a/s/e/c) (#187)
|
|
138
|
+
- Tags are re-pointed to HEAD after library commit so tagged releases include regenerated books (#187)
|
|
139
|
+
|
|
140
|
+
### 🐛 Fixed
|
|
141
|
+
- Fixed bump-version crashing on non-semver version files (e.g., Maven `${revision}`) by excluding them from version resolution (#187)
|
|
142
|
+
- Fixed create-pr library pipeline leaking git-hooks paths into foreign repos (#186)
|
|
143
|
+
- Fixed local tag lookup scanning all branches instead of scoping to HEAD branch (#185)
|
|
144
|
+
- Fixed stale tags in create-pr when library pipeline generates a new commit after tag creation (#187)
|
|
145
|
+
|
|
146
|
+
### 🗑️ Removed
|
|
147
|
+
- Removed per-file custom version editing (option `e`) from bump-version interactive selection (#187)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
## [2.64.1] - 2026-06-08
|
|
151
|
+
|
|
152
|
+
### ✨ Added
|
|
153
|
+
- Multi-language extractor registry with pluggable architecture for Library book generation [AUT-3742] (#184)
|
|
154
|
+
- JS extractor as first pluggable extractor with Tree-sitter parsing, extractor notes template, and test harness [AUT-3742] (#184)
|
|
155
|
+
- ESLint rule preventing `lib/` from statically importing `.library/` modules [AUT-3742] (#184)
|
|
156
|
+
- Branch-scoped tag lookup functions `getLatestLocalTagOnBranch()` and `getLatestRemoteTagOnBranch()` in git-tag-manager (#185)
|
|
157
|
+
|
|
158
|
+
### 🔧 Changed
|
|
159
|
+
- Library pipeline (`createPrPipeline`) now derives `booksDir`/`sourceDir` from overridden `repoRoot`, preventing git-hooks paths from leaking into foreign repos (#186)
|
|
160
|
+
- Version alignment scoped to base branch tags instead of global tag list (#185)
|
|
161
|
+
- Bump-version interactive file selection simplified to direct toggle list with non-semver files pre-deselected
|
|
162
|
+
- Moved arrow-function extraction from monolithic `extract.js` to pluggable extractor module [AUT-3742]
|
|
163
|
+
|
|
164
|
+
### 🐛 Fixed
|
|
165
|
+
- Excluded non-semver files (e.g., Maven `${revision}`) from version resolution in bump-version command, preventing `parseVersion()` failures
|
|
166
|
+
- Fixed library pipeline using hardcoded git-hooks paths when running in foreign repos (#186)
|
|
167
|
+
- Scoped local tag lookup to HEAD branch to avoid cross-branch version pollution (#185)
|
|
168
|
+
|
|
169
|
+
### 🗑️ Removed
|
|
170
|
+
- Removed menu-based file selection (all/select/edit/cancel) from bump-version in favor of direct toggle list
|
|
171
|
+
- Removed per-file custom version entry (`targetVersion`) from interactive file selection
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
## [2.64.0] - 2026-06-08
|
|
175
|
+
|
|
176
|
+
### ✨ Added
|
|
177
|
+
- Multi-language extractor registry with pluggable interface and JS extractor (AUT-3742)
|
|
178
|
+
- Extractor notes template for documenting per-language extractors (AUT-3742)
|
|
179
|
+
- Test harness with JS fixtures for extractor validation (AUT-3742)
|
|
180
|
+
- Branch-scoped tag lookup methods `getLatestLocalTagOnBranch()` and `getLatestRemoteTagOnBranch()` in git-tag-manager
|
|
181
|
+
- ESLint `no-restricted-imports` rule preventing `lib/` from statically importing `.library/` modules (AUT-3742)
|
|
182
|
+
|
|
183
|
+
### 🔧 Changed
|
|
184
|
+
- Moved arrow-function extraction from monolithic `extract.js` to pluggable extractor module under `librarian/extractors/registered/js/` (AUT-3742)
|
|
185
|
+
- `validateVersionAlignment()` now accepts a `baseBranch` parameter, scoping version checks to the relevant branch tags instead of all tags
|
|
186
|
+
- Library pipeline (`createPrPipeline`) now derives `booksDir`/`sourceDir` from resolver config when `repoRoot` is overridden, preventing path leakage into foreign repos
|
|
187
|
+
|
|
188
|
+
### 🐛 Fixed
|
|
189
|
+
- Fixed local tag lookup returning tags from unrelated branches instead of scoping to HEAD branch
|
|
190
|
+
- Fixed library pipeline leaking git-hooks internal paths into foreign repos during PR creation (#185)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
## [2.63.1] - 2026-06-03
|
|
194
|
+
|
|
195
|
+
### ✨ Added
|
|
196
|
+
|
|
197
|
+
- Multi-language extractor registry with pluggable interface and runtime validation (AUT-3742, #184)
|
|
198
|
+
- JS extractor as first pluggable extractor with full EXTRACTOR_NOTES documentation (AUT-3742)
|
|
199
|
+
- Extractor notes template for documenting new language extractors (AUT-3742)
|
|
200
|
+
- Test harness with JS fixtures for extractor validation (AUT-3742)
|
|
201
|
+
- Version alignment scoped to base branch tags for accurate cross-branch versioning
|
|
202
|
+
|
|
203
|
+
### 🔧 Changed
|
|
204
|
+
|
|
205
|
+
- Decoupled lib/ from .library/ imports with ESLint no-restricted-imports rule to enforce package boundary (AUT-3742)
|
|
206
|
+
- Refactored arrow-function extraction from monolithic extract.js into pluggable JS extractor module (AUT-3742)
|
|
207
|
+
|
|
208
|
+
### 🐛 Fixed
|
|
209
|
+
|
|
210
|
+
- Scoped local tag lookup to HEAD branch to prevent cross-branch version conflicts
|
|
211
|
+
|
|
212
|
+
## [2.63.0] - 2026-06-03
|
|
213
|
+
|
|
214
|
+
### ✨ Added
|
|
215
|
+
|
|
216
|
+
- Added multi-language extractor registry with convention-driven loader, priority-based lookup, and warn-and-skip error handling (AUT-3742)
|
|
217
|
+
- Added Extractor interface definition with JSDoc types and runtime validator for pluggable per-language extractors (AUT-3742)
|
|
218
|
+
- Added JS/ESM pluggable extractor conforming to the new Extractor interface (AUT-3742)
|
|
219
|
+
- Added extractor notes template and canonical JS extractor documentation covering supported constructs, known limitations, and test fixtures (AUT-3742)
|
|
220
|
+
- Added ESLint `no-restricted-imports` rule preventing `lib/` from statically importing `.library/` modules (AUT-3742)
|
|
221
|
+
- Added extractor test harness with JS fixtures for eight construct categories (AUT-3742)
|
|
222
|
+
|
|
223
|
+
### 🔧 Changed
|
|
224
|
+
|
|
225
|
+
- Refactored arrow-function extraction from monolithic `extract.js` into standalone pluggable extractor module under `librarian/extractors/registered/js/` (AUT-3742)
|
|
226
|
+
- Decoupled `lib/` source code from `.library/` imports — runtime integration now requires dynamic `import()` with graceful degradation (AUT-3742)
|
|
227
|
+
|
|
228
|
+
## [2.62.0] - 2026-06-02
|
|
229
|
+
|
|
230
|
+
### ✨ Added
|
|
231
|
+
|
|
232
|
+
- Added structure detector pipeline for Library bootstrap — 3-stage detection (filesystem scan → Haiku inference → interactive console) that generates `.library/structure.yaml` config (AUT-3741)
|
|
233
|
+
- Added deterministic filesystem scanner with configurable depth/child limits, BOM file detection, and language-by-layer counting (AUT-3741)
|
|
234
|
+
- Added Haiku-based structure inference with exponential-backoff retry, scanner-detected language reconciliation, and graceful degradation to stub config on API failure (AUT-3741)
|
|
235
|
+
- Added interactive 3-option console flow (Accept / Reject / Retry) with re-run safety supporting Keep / Overwrite / Merge of existing configs (AUT-3741)
|
|
236
|
+
- Added structure detector documentation to Library README including sequence diagram, consumer contract, config format, and worked example (AUT-3741)
|
|
237
|
+
|
|
238
|
+
### 🐛 Fixed
|
|
239
|
+
|
|
240
|
+
- Fixed tag staleness during Library regeneration (#179)
|
|
241
|
+
|
|
8
242
|
## [2.61.2] - 2026-06-01
|
|
9
243
|
|
|
10
244
|
### 🐛 Fixed
|
|
11
|
-
- Fixed incorrect import path for gotcha solicitation in PR pipeline
|
|
12
245
|
|
|
246
|
+
- Fixed incorrect import path for gotcha solicitation in PR pipeline
|
|
13
247
|
|
|
14
248
|
## [2.61.1] - 2026-06-01
|
|
15
249
|
|
|
16
250
|
### ✨ Added
|
|
251
|
+
|
|
17
252
|
- Auto-regeneration of stale Library books during `create-release` — stale books are now regenerated before the release is tagged, keeping the tag accurate (#178, AUT-3738)
|
|
18
253
|
|
|
19
254
|
### 🔧 Changed
|
|
255
|
+
|
|
20
256
|
- Staleness warning templates are now context-aware: `CONSOLE_WARNING_TEMPLATE` and `PR_BODY_SECTION_TEMPLATE` accept an `autoRegen` option to tailor messaging per consumer (`will-run` for create-release, `deferred` for bump-version, `completed`/`failed` in PR body)
|
|
21
257
|
- PR body staleness section now reflects the actual auto-regeneration outcome instead of always listing remediation scripts
|
|
22
258
|
- Console staleness warning in `bump-version` now tells the user that Library will be auto-regenerated when they run `create-pr`
|
|
23
259
|
|
|
24
260
|
### 🗑️ Removed
|
|
25
|
-
- Removed static recommended-scripts list from console staleness warnings (replaced by context-aware auto-regeneration messages)
|
|
26
261
|
|
|
262
|
+
- Removed static recommended-scripts list from console staleness warnings (replaced by context-aware auto-regeneration messages)
|
|
27
263
|
|
|
28
264
|
## [2.61.0] - 2026-06-01
|
|
29
265
|
|
|
30
266
|
### ✨ Added
|
|
267
|
+
|
|
31
268
|
- Added canonical staleness-warning templates (`CONSOLE_WARNING_TEMPLATE`, `PR_BODY_SECTION_TEMPLATE`, `PR_TAG_VALUE`) for consistent Library staleness messaging across all consumer commands (AUT-3738)
|
|
32
269
|
- Added non-blocking Library staleness verification gate to the `create-release` workflow — warns operators when Library books are out of date without blocking the release (AUT-3738)
|
|
33
270
|
- Added automatic release PR creation step in `create-release` with idempotency check, staleness section in PR body wrapped in marker comments, and `library-stale` label when applicable (AUT-3738)
|
|
34
271
|
- Added `LIBRARY_VERIFY_SKIPPED_WARNING_RELEASE` constant for release-specific Library verification skip messaging (AUT-3738)
|
|
35
272
|
|
|
36
273
|
### 🔧 Changed
|
|
274
|
+
|
|
37
275
|
- Refactored `bump-version` Library warning to use the canonical `CONSOLE_WARNING_TEMPLATE` instead of inline formatting (AUT-3738)
|
|
38
276
|
- Changed `create-release` Library staleness gate from blocking (exit on stale) to non-blocking (warn and continue), using the librarian `verify()` API instead of raw staleness tools (AUT-3738)
|
|
39
277
|
- Refactored `library-warnings.js` to re-export canonical wording from the librarian messages module instead of defining constants inline (AUT-3738)
|
|
40
278
|
- Updated Library books for `create-release`, `bump-version`, and `library-warnings` to reflect new dependencies and exports (AUT-3738)
|
|
41
279
|
|
|
42
280
|
### 🗑️ Removed
|
|
281
|
+
|
|
43
282
|
- Removed blocking Library staleness check from `create-release` that imported `.library/tools/staleness.js` directly and aborted on drift (AUT-3738)
|
|
44
283
|
- Removed `LIBRARY_STALE_WARNING` constant from `library-warnings.js`, replaced by canonical `CONSOLE_WARNING_TEMPLATE` (AUT-3738)
|
|
45
284
|
|
|
46
|
-
|
|
47
285
|
## [2.61.0] - 2026-05-29
|
|
48
286
|
|
|
49
287
|
### ✨ Added
|
|
@@ -48,9 +48,7 @@ import {
|
|
|
48
48
|
showError,
|
|
49
49
|
showWarning,
|
|
50
50
|
promptConfirmation,
|
|
51
|
-
|
|
52
|
-
promptToggleList,
|
|
53
|
-
promptEditField
|
|
51
|
+
promptToggleList
|
|
54
52
|
} from '../utils/interactive-ui.js';
|
|
55
53
|
import logger from '../utils/logger.js';
|
|
56
54
|
import { colors, error, checkGitRepo } from './helpers.js';
|
|
@@ -209,7 +207,9 @@ function displayDiscoveryTable(discovery) {
|
|
|
209
207
|
);
|
|
210
208
|
console.log('');
|
|
211
209
|
|
|
212
|
-
|
|
210
|
+
const visibleFiles = discovery.files.filter((f) => f.selected);
|
|
211
|
+
|
|
212
|
+
if (visibleFiles.length === 0) {
|
|
213
213
|
console.log(' No version files found.');
|
|
214
214
|
console.log('');
|
|
215
215
|
return;
|
|
@@ -221,8 +221,8 @@ function displayDiscoveryTable(discovery) {
|
|
|
221
221
|
);
|
|
222
222
|
console.log(` ${'-'.repeat(3)} ${'-'.repeat(35)} ${'-'.repeat(15)} ${'-'.repeat(15)}`);
|
|
223
223
|
|
|
224
|
-
// Table rows
|
|
225
|
-
|
|
224
|
+
// Table rows (only semver files)
|
|
225
|
+
visibleFiles.forEach((file, index) => {
|
|
226
226
|
const num = `${index + 1}`.padEnd(3);
|
|
227
227
|
const filePath = file.relativePath.padEnd(35);
|
|
228
228
|
const fileType = file.projectLabel.padEnd(15);
|
|
@@ -293,70 +293,20 @@ function showDryRunPreview(info) {
|
|
|
293
293
|
*/
|
|
294
294
|
async function promptFileSelection(discovery) {
|
|
295
295
|
console.log('');
|
|
296
|
-
showWarning('Version mismatch detected or interactive mode enabled');
|
|
297
|
-
console.log('');
|
|
298
|
-
|
|
299
|
-
const options = [
|
|
300
|
-
{ key: 'a', label: 'Update all files' },
|
|
301
|
-
{ key: 's', label: 'Select which files to update' },
|
|
302
|
-
{ key: 'e', label: 'Edit version per file (advanced)' },
|
|
303
|
-
{ key: 'c', label: 'Cancel' }
|
|
304
|
-
];
|
|
305
|
-
|
|
306
|
-
const choice = await promptMenu('Choose action:', options, 'a');
|
|
307
|
-
|
|
308
|
-
if (choice === 'c') {
|
|
309
|
-
showInfo('Version bump cancelled');
|
|
310
|
-
process.exit(0);
|
|
311
|
-
}
|
|
312
296
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
// Toggle list selection
|
|
320
|
-
const items = discovery.files.map((file, index) => ({
|
|
321
|
-
key: `${index}`,
|
|
322
|
-
label: `${file.relativePath} (${file.projectLabel}) - ${file.version || 'N/A'}`,
|
|
323
|
-
selected: file.selected
|
|
324
|
-
}));
|
|
325
|
-
|
|
326
|
-
const selectedKeys = await promptToggleList(
|
|
327
|
-
'Select files to update (type number to toggle, Enter to confirm):',
|
|
328
|
-
items
|
|
329
|
-
);
|
|
330
|
-
|
|
331
|
-
// Filter files by selected keys
|
|
332
|
-
return discovery.files.filter((_, index) => selectedKeys.includes(`${index}`));
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (choice === 'e') {
|
|
336
|
-
// Per-file version editing
|
|
337
|
-
showInfo('Enter target version for each file (press Enter to keep calculated version)');
|
|
338
|
-
console.log('');
|
|
339
|
-
|
|
340
|
-
for (const file of discovery.files) {
|
|
341
|
-
const input = await promptEditField(
|
|
342
|
-
`${file.relativePath} (${file.projectLabel})`,
|
|
343
|
-
file.version || 'N/A'
|
|
344
|
-
);
|
|
297
|
+
const semverFiles = discovery.files.filter((f) => f.selected);
|
|
298
|
+
const items = semverFiles.map((file, index) => ({
|
|
299
|
+
key: `${index}`,
|
|
300
|
+
label: `${file.relativePath} (${file.projectLabel}) - ${file.version || 'N/A'}`,
|
|
301
|
+
selected: true
|
|
302
|
+
}));
|
|
345
303
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
continue;
|
|
351
|
-
}
|
|
352
|
-
file.targetVersion = input;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
return discovery.files;
|
|
357
|
-
}
|
|
304
|
+
const selectedKeys = await promptToggleList(
|
|
305
|
+
'Select files to update (type number to toggle, Enter to confirm):',
|
|
306
|
+
items
|
|
307
|
+
);
|
|
358
308
|
|
|
359
|
-
return
|
|
309
|
+
return semverFiles.filter((_, index) => selectedKeys.includes(`${index}`));
|
|
360
310
|
}
|
|
361
311
|
|
|
362
312
|
/**
|
|
@@ -504,15 +454,6 @@ export async function runBumpVersion(args) {
|
|
|
504
454
|
// Display discovery table
|
|
505
455
|
displayDiscoveryTable(discovery);
|
|
506
456
|
|
|
507
|
-
const currentVersion = discovery.resolvedVersion;
|
|
508
|
-
|
|
509
|
-
if (!currentVersion) {
|
|
510
|
-
showError('Could not determine current version from discovered files');
|
|
511
|
-
process.exit(1);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
showInfo(`Current version: ${currentVersion}`);
|
|
515
|
-
|
|
516
457
|
// Step 3: File selection (if mismatch or interactive mode)
|
|
517
458
|
let selectedFiles = discovery.files.filter((f) => f.selected);
|
|
518
459
|
|
|
@@ -528,6 +469,22 @@ export async function runBumpVersion(args) {
|
|
|
528
469
|
console.log('');
|
|
529
470
|
}
|
|
530
471
|
|
|
472
|
+
// Derive current version from selected files (not from discovery.resolvedVersion)
|
|
473
|
+
const selectedVersions = selectedFiles
|
|
474
|
+
.filter((f) => f.version !== null)
|
|
475
|
+
.map((f) => f.version);
|
|
476
|
+
const uniqueSelected = [...new Set(selectedVersions)];
|
|
477
|
+
const currentVersion = uniqueSelected.length === 1
|
|
478
|
+
? uniqueSelected[0]
|
|
479
|
+
: discovery.resolvedVersion;
|
|
480
|
+
|
|
481
|
+
if (!currentVersion || !validateVersionFormat(currentVersion)) {
|
|
482
|
+
showError('Could not determine a valid semver version from selected files');
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
showInfo(`Current version: ${currentVersion}`);
|
|
487
|
+
|
|
531
488
|
// Step 4: Calculate new version or apply suffix operation
|
|
532
489
|
logger.debug('bump-version', 'Step 4: Calculating new version');
|
|
533
490
|
let newVersion;
|
|
@@ -364,7 +364,7 @@ export async function runCreatePr(args) {
|
|
|
364
364
|
// Step 5.6: Version alignment validation (Issue #44)
|
|
365
365
|
logger.debug('create-pr', 'Step 5.6: Validating version alignment');
|
|
366
366
|
const { validateVersionAlignment } = await import('../utils/version-manager.js');
|
|
367
|
-
const versionCheck = await validateVersionAlignment();
|
|
367
|
+
const versionCheck = await validateVersionAlignment(baseBranch);
|
|
368
368
|
|
|
369
369
|
if (!versionCheck.aligned) {
|
|
370
370
|
showWarning('Version misalignment detected:');
|
|
@@ -451,23 +451,135 @@ export async function runCreatePr(args) {
|
|
|
451
451
|
}
|
|
452
452
|
}
|
|
453
453
|
|
|
454
|
-
// Step 5.7:
|
|
455
|
-
|
|
454
|
+
// Step 5.7: Library maintenance pipeline (AUT-3764)
|
|
455
|
+
// Runs BEFORE tag push so that library regen is included in the tagged commit.
|
|
456
|
+
// Guard: only run if the current repo has its own .library/ setup.
|
|
457
|
+
// When claude-hooks is installed in a foreign repo (npm link / file: ref),
|
|
458
|
+
// the relative import resolves to git-hooks' .library/ — not this repo's.
|
|
459
|
+
let libraryCommitted = false;
|
|
460
|
+
const root = getRepoRoot();
|
|
461
|
+
const libraryResolverPath = path.join(root, '.library', 'resolver.yaml');
|
|
462
|
+
if (!fs.existsSync(libraryResolverPath)) {
|
|
463
|
+
logger.debug('create-pr', 'No .library/resolver.yaml in current repo — skipping Library pipeline');
|
|
464
|
+
} else {
|
|
465
|
+
logger.debug('create-pr', 'Step 5.7: Running Library maintenance pipeline');
|
|
466
|
+
try {
|
|
467
|
+
showInfo('Running Library maintenance pipeline...');
|
|
468
|
+
const { createPrPipeline } = await import('../../.library/librarian/index.js');
|
|
469
|
+
|
|
470
|
+
const pipelineSummary = await createPrPipeline({ repoRoot: root });
|
|
471
|
+
const {
|
|
472
|
+
modifiedFiles: libraryFiles,
|
|
473
|
+
perStep,
|
|
474
|
+
pendingDueToApiDown,
|
|
475
|
+
warnings: pipelineWarnings,
|
|
476
|
+
} = pipelineSummary;
|
|
477
|
+
|
|
478
|
+
// Surface pipeline summary
|
|
479
|
+
logger.debug('create-pr', 'Pipeline completed', {
|
|
480
|
+
modifiedCount: libraryFiles.length,
|
|
481
|
+
pendingDueToApiDown,
|
|
482
|
+
warningCount: pipelineWarnings.length,
|
|
483
|
+
});
|
|
484
|
+
showInfo(`Staleness: ${perStep.staleness.staleCount} stale, ${perStep.staleness.unbookedCount} unbooked`);
|
|
485
|
+
if (perStep.regen.changed > 0) {
|
|
486
|
+
showInfo(`Regenerated: ${perStep.regen.changed} book(s)`);
|
|
487
|
+
}
|
|
488
|
+
if (perStep.addRemoveRename.created > 0) {
|
|
489
|
+
showInfo(`Created: ${perStep.addRemoveRename.created} new book(s)`);
|
|
490
|
+
}
|
|
491
|
+
if (pendingDueToApiDown > 0) {
|
|
492
|
+
showWarning(`Gotchas pending (API down): ${pendingDueToApiDown}`);
|
|
493
|
+
}
|
|
494
|
+
for (const w of pipelineWarnings) {
|
|
495
|
+
showWarning(w);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (libraryFiles.length === 0) {
|
|
499
|
+
showInfo('Library already in sync');
|
|
500
|
+
} else {
|
|
501
|
+
// Stage all modified Library files (stageFiles expects absolute paths)
|
|
502
|
+
const absFiles = libraryFiles.map(f => path.join(root, f));
|
|
503
|
+
const stageResult = stageFiles(absFiles);
|
|
504
|
+
|
|
505
|
+
if (!stageResult.success) {
|
|
506
|
+
showWarning(`Failed to stage Library files: ${stageResult.error}`);
|
|
507
|
+
} else {
|
|
508
|
+
// Commit with deterministic message — separate commit, not amend
|
|
509
|
+
const libraryCommitMsg = `chore(library): sync books for ${currentBranch}`;
|
|
510
|
+
const commitResult = createCommit(libraryCommitMsg);
|
|
511
|
+
|
|
512
|
+
if (!commitResult.success) {
|
|
513
|
+
showWarning(`Failed to commit Library changes: ${commitResult.error}`);
|
|
514
|
+
} else {
|
|
515
|
+
libraryCommitted = true;
|
|
516
|
+
showSuccess(`Library committed: ${libraryCommitMsg} (${libraryFiles.length} file(s))`);
|
|
517
|
+
|
|
518
|
+
// Push the Library commit to the PR branch's remote
|
|
519
|
+
const libraryPushResult = pushBranch(currentBranch);
|
|
520
|
+
if (!libraryPushResult.success) {
|
|
521
|
+
showWarning(`Failed to push Library commit: ${libraryPushResult.error}`);
|
|
522
|
+
} else {
|
|
523
|
+
showSuccess('Library commit pushed');
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
} catch (pipelineErr) {
|
|
529
|
+
// Pipeline failure is non-blocking — log and continue with PR creation
|
|
530
|
+
logger.warning('create-pr', 'Library pipeline failed, continuing', {
|
|
531
|
+
error: pipelineErr.message,
|
|
532
|
+
});
|
|
533
|
+
showWarning(`Library pipeline unavailable: ${pipelineErr.message}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Step 5.8: Smart tag pushing (Issue #44)
|
|
538
|
+
// Runs AFTER library maintenance so tags include the library commit.
|
|
539
|
+
logger.debug('create-pr', 'Step 5.8: Checking and pushing unpushed tags');
|
|
456
540
|
const {
|
|
457
541
|
compareLocalAndRemoteTags,
|
|
458
542
|
pushTags: pushTagsUtil,
|
|
459
|
-
|
|
460
|
-
|
|
543
|
+
createTag: createTagUtil,
|
|
544
|
+
getLatestRemoteTagOnBranch,
|
|
461
545
|
parseTagVersion
|
|
462
546
|
} = await import('../utils/git-tag-manager.js');
|
|
463
547
|
const { compareVersions } = await import('../utils/version-manager.js');
|
|
464
548
|
|
|
549
|
+
// If library was committed after bump-version created the tag,
|
|
550
|
+
// re-point unpushed tags to HEAD so the tag includes library books.
|
|
551
|
+
if (libraryCommitted) {
|
|
552
|
+
const unpushedTags = await compareLocalAndRemoteTags();
|
|
553
|
+
for (const tag of unpushedTags.localNewer) {
|
|
554
|
+
const version = parseTagVersion(tag);
|
|
555
|
+
if (version) {
|
|
556
|
+
logger.debug('create-pr', 'Re-pointing tag to include library commit', { tag });
|
|
557
|
+
let origTagMsg = '';
|
|
558
|
+
try { origTagMsg = execSync(`git tag -l --format="%(contents)" ${tag}`, { encoding: 'utf8' }).trim(); } catch { /* ignore */ }
|
|
559
|
+
const tagMsg = origTagMsg || `Release version ${version}`;
|
|
560
|
+
try {
|
|
561
|
+
await createTagUtil(version, tagMsg, { force: true });
|
|
562
|
+
showInfo(`Tag ${tag} moved to include library commit`);
|
|
563
|
+
} catch (tagErr) {
|
|
564
|
+
logger.warning('create-pr', 'Failed to re-point tag, continuing', { tag, error: tagErr.message });
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
465
570
|
const tagComparison = await compareLocalAndRemoteTags();
|
|
466
571
|
|
|
467
572
|
if (tagComparison.localNewer.length > 0) {
|
|
468
|
-
//
|
|
469
|
-
const
|
|
470
|
-
|
|
573
|
+
// Derive latest unpushed tag (scoped to what's actually being pushed)
|
|
574
|
+
const sortedUnpushed = tagComparison.localNewer
|
|
575
|
+
.map((t) => ({ tag: t, version: parseTagVersion(t) }))
|
|
576
|
+
.filter((t) => t.version !== null)
|
|
577
|
+
.sort((a, b) => compareVersions(a.version, b.version));
|
|
578
|
+
const latestLocalTag = sortedUnpushed.length > 0
|
|
579
|
+
? sortedUnpushed[sortedUnpushed.length - 1].tag
|
|
580
|
+
: tagComparison.localNewer[0];
|
|
581
|
+
// Compare against latest remote tag on the BASE branch (not global latest)
|
|
582
|
+
const latestRemoteTag = getLatestRemoteTagOnBranch(baseBranch);
|
|
471
583
|
|
|
472
584
|
const localVersion = latestLocalTag ? parseTagVersion(latestLocalTag) : null;
|
|
473
585
|
const remoteVersion = latestRemoteTag ? parseTagVersion(latestRemoteTag) : null;
|
|
@@ -483,7 +595,7 @@ export async function runCreatePr(args) {
|
|
|
483
595
|
let shouldPushTags = false;
|
|
484
596
|
let userChoice = null;
|
|
485
597
|
|
|
486
|
-
// Case 1: Local tag > Remote tag → Auto-push
|
|
598
|
+
// Case 1: Local tag > Remote tag → Auto-push (normal bump-version flow)
|
|
487
599
|
if (localVersion && remoteVersion && compareVersions(localVersion, remoteVersion) > 0) {
|
|
488
600
|
logger.debug('create-pr', 'Local version > remote version, auto-pushing', {
|
|
489
601
|
localVersion,
|
|
@@ -494,7 +606,7 @@ export async function runCreatePr(args) {
|
|
|
494
606
|
showInfo('Auto-pushing tag to remote...');
|
|
495
607
|
shouldPushTags = true;
|
|
496
608
|
|
|
497
|
-
// Case 2: Local tag = Remote tag → Prompt with warning
|
|
609
|
+
// Case 2: Local tag = Remote tag → Prompt with warning (may already be pushed)
|
|
498
610
|
} else if (
|
|
499
611
|
localVersion &&
|
|
500
612
|
remoteVersion &&
|
|
@@ -534,7 +646,7 @@ export async function runCreatePr(args) {
|
|
|
534
646
|
shouldPushTags = true;
|
|
535
647
|
}
|
|
536
648
|
|
|
537
|
-
// Case 3: Local tag < Remote tag → Prompt with
|
|
649
|
+
// Case 3: Local tag < Remote tag → Prompt with warning (someone pushed newer)
|
|
538
650
|
} else if (
|
|
539
651
|
localVersion &&
|
|
540
652
|
remoteVersion &&
|
|
@@ -664,78 +776,6 @@ export async function runCreatePr(args) {
|
|
|
664
776
|
logger.debug('create-pr', 'No unpushed tags found, continuing');
|
|
665
777
|
}
|
|
666
778
|
|
|
667
|
-
// Step 5.8: Library maintenance pipeline (AUT-3764)
|
|
668
|
-
logger.debug('create-pr', 'Step 5.8: Running Library maintenance pipeline');
|
|
669
|
-
try {
|
|
670
|
-
showInfo('Running Library maintenance pipeline...');
|
|
671
|
-
const { createPrPipeline } = await import('../../.library/librarian/index.js');
|
|
672
|
-
const root = getRepoRoot();
|
|
673
|
-
|
|
674
|
-
const pipelineSummary = await createPrPipeline({ repoRoot: root });
|
|
675
|
-
const {
|
|
676
|
-
modifiedFiles: libraryFiles,
|
|
677
|
-
perStep,
|
|
678
|
-
pendingDueToApiDown,
|
|
679
|
-
warnings: pipelineWarnings,
|
|
680
|
-
} = pipelineSummary;
|
|
681
|
-
|
|
682
|
-
// Surface pipeline summary
|
|
683
|
-
logger.debug('create-pr', 'Pipeline completed', {
|
|
684
|
-
modifiedCount: libraryFiles.length,
|
|
685
|
-
pendingDueToApiDown,
|
|
686
|
-
warningCount: pipelineWarnings.length,
|
|
687
|
-
});
|
|
688
|
-
showInfo(`Staleness: ${perStep.staleness.staleCount} stale, ${perStep.staleness.unbookedCount} unbooked`);
|
|
689
|
-
if (perStep.regen.changed > 0) {
|
|
690
|
-
showInfo(`Regenerated: ${perStep.regen.changed} book(s)`);
|
|
691
|
-
}
|
|
692
|
-
if (perStep.addRemoveRename.created > 0) {
|
|
693
|
-
showInfo(`Created: ${perStep.addRemoveRename.created} new book(s)`);
|
|
694
|
-
}
|
|
695
|
-
if (pendingDueToApiDown > 0) {
|
|
696
|
-
showWarning(`Gotchas pending (API down): ${pendingDueToApiDown}`);
|
|
697
|
-
}
|
|
698
|
-
for (const w of pipelineWarnings) {
|
|
699
|
-
showWarning(w);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
if (libraryFiles.length === 0) {
|
|
703
|
-
showInfo('Library already in sync');
|
|
704
|
-
} else {
|
|
705
|
-
// Stage all modified Library files (stageFiles expects absolute paths)
|
|
706
|
-
const absFiles = libraryFiles.map(f => path.join(root, f));
|
|
707
|
-
const stageResult = stageFiles(absFiles);
|
|
708
|
-
|
|
709
|
-
if (!stageResult.success) {
|
|
710
|
-
showWarning(`Failed to stage Library files: ${stageResult.error}`);
|
|
711
|
-
} else {
|
|
712
|
-
// Commit with deterministic message — separate commit, not amend
|
|
713
|
-
const libraryCommitMsg = `chore(library): sync books for ${currentBranch}`;
|
|
714
|
-
const commitResult = createCommit(libraryCommitMsg);
|
|
715
|
-
|
|
716
|
-
if (!commitResult.success) {
|
|
717
|
-
showWarning(`Failed to commit Library changes: ${commitResult.error}`);
|
|
718
|
-
} else {
|
|
719
|
-
showSuccess(`Library committed: ${libraryCommitMsg} (${libraryFiles.length} file(s))`);
|
|
720
|
-
|
|
721
|
-
// Push the Library commit to the PR branch's remote
|
|
722
|
-
const libraryPushResult = pushBranch(currentBranch);
|
|
723
|
-
if (!libraryPushResult.success) {
|
|
724
|
-
showWarning(`Failed to push Library commit: ${libraryPushResult.error}`);
|
|
725
|
-
} else {
|
|
726
|
-
showSuccess('Library commit pushed');
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
} catch (pipelineErr) {
|
|
732
|
-
// Pipeline failure is non-blocking — log and continue with PR creation
|
|
733
|
-
logger.warning('create-pr', 'Library pipeline failed, continuing', {
|
|
734
|
-
error: pipelineErr.message,
|
|
735
|
-
});
|
|
736
|
-
showWarning(`Library pipeline unavailable: ${pipelineErr.message}`);
|
|
737
|
-
}
|
|
738
|
-
|
|
739
779
|
// Step 6: Generate PR metadata using engine
|
|
740
780
|
logger.debug('create-pr', 'Step 6: Generating PR metadata with engine');
|
|
741
781
|
showInfo('Generating PR metadata with Claude...');
|
package/lib/commands/help.js
CHANGED
|
@@ -18,8 +18,6 @@ import { fetchFileContent, fetchDirectoryListing, createIssue } from '../utils/g
|
|
|
18
18
|
import { promptMenu, promptEditField, promptConfirmation } from '../utils/interactive-ui.js';
|
|
19
19
|
import logger from '../utils/logger.js';
|
|
20
20
|
import { commands } from '../cli-metadata.js';
|
|
21
|
-
import { fetchLibraryContent as librarianFetch } from '../../.library/librarian/index.js';
|
|
22
|
-
|
|
23
21
|
/**
|
|
24
22
|
* Get claude-hooks source repo coordinates from package.json
|
|
25
23
|
* Why: AI help must always fetch from the tool's own repo, not the user's current repo
|
|
@@ -192,10 +190,22 @@ const _readPackageFile = async (relativePath) => {
|
|
|
192
190
|
* Why: The catalog provides navigational context for the AI librarian (Pass 1).
|
|
193
191
|
* Delegates to the librarian module for directory discovery and routing.
|
|
194
192
|
*
|
|
193
|
+
* Uses dynamic import() because .library/ is not shipped in the npm package —
|
|
194
|
+
* it only exists in the source repo. When running from a global install,
|
|
195
|
+
* the import fails gracefully and the help command falls back to static help.
|
|
196
|
+
*
|
|
195
197
|
* @returns {Promise<string|null>} Concatenated catalog or null if nothing could be read
|
|
196
198
|
*/
|
|
197
199
|
const readLibraryCatalog = async () => {
|
|
198
|
-
|
|
200
|
+
let librarianFetch;
|
|
201
|
+
try {
|
|
202
|
+
const mod = await import('../../.library/librarian/index.js');
|
|
203
|
+
librarianFetch = mod.fetchLibraryContent;
|
|
204
|
+
} catch {
|
|
205
|
+
logger.debug('help - readLibraryCatalog', 'Library not available (expected in global install)');
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
199
209
|
try {
|
|
200
210
|
const result = await librarianFetch(null, { repoRoot: _packageRoot, full: true });
|
|
201
211
|
if (result.catalog) {
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* File: library-warnings.js
|
|
3
|
-
* Purpose:
|
|
3
|
+
* Purpose: Staleness-warning wording and rendering for Library verification
|
|
4
|
+
* gates in claude-hooks.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
* (
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* These are consumer rendering functions — they take structured VerifyResult
|
|
7
|
+
* data from the Library and produce display-ready text (console) or Markdown
|
|
8
|
+
* (PR body). The Library produces the data; this module formats it.
|
|
9
|
+
*
|
|
10
|
+
* Constraints:
|
|
11
|
+
* - Pure functions of (VerifyResult, opts) — no I/O, no globals, no environment
|
|
12
|
+
* - Do NOT import from .library/ — lib/ must not depend on unshipped paths
|
|
9
13
|
*
|
|
10
14
|
* Related tickets:
|
|
11
15
|
* AUT-3767 — original placeholder (retired by AUT-3769)
|
|
@@ -14,11 +18,125 @@
|
|
|
14
18
|
* AUT-3738 — parent user story
|
|
15
19
|
*/
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Render an assertive console warning for Library staleness.
|
|
23
|
+
*
|
|
24
|
+
* Returns plain text with a marker and the list of stale books.
|
|
25
|
+
* The closing section varies by consumer context via `opts.autoRegen`:
|
|
26
|
+
* - `'will-run'` — create-release: regen runs in the same command
|
|
27
|
+
* - `'deferred'` — bump-version: regen runs later via create-pr
|
|
28
|
+
*
|
|
29
|
+
* Consumers apply visual formatting (box, color, stderr routing) on top.
|
|
30
|
+
*
|
|
31
|
+
* Returns an empty string when the Library is clean.
|
|
32
|
+
*
|
|
33
|
+
* @param {Object} verifyResult
|
|
34
|
+
* @param {boolean} verifyResult.clean
|
|
35
|
+
* @param {Array<{ path: string, reasons: string[] }>} verifyResult.staleBooks
|
|
36
|
+
* @param {string[]} verifyResult.recommendedScripts
|
|
37
|
+
* @param {Object} opts
|
|
38
|
+
* @param {'will-run'|'deferred'} opts.autoRegen — controls the call-to-action
|
|
39
|
+
* @returns {string} Plain-text console warning, or empty string if clean
|
|
40
|
+
*/
|
|
41
|
+
export function CONSOLE_WARNING_TEMPLATE(verifyResult, opts = {}) {
|
|
42
|
+
if (verifyResult.clean) {
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const lines = [];
|
|
47
|
+
|
|
48
|
+
lines.push('⚠️ Library is stale');
|
|
49
|
+
lines.push('');
|
|
50
|
+
lines.push('The following books are out of date:');
|
|
51
|
+
lines.push('');
|
|
52
|
+
|
|
53
|
+
for (const book of verifyResult.staleBooks) {
|
|
54
|
+
lines.push(` • ${book.path}`);
|
|
55
|
+
for (const reason of book.reasons) {
|
|
56
|
+
lines.push(` - ${reason}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
lines.push('');
|
|
61
|
+
|
|
62
|
+
if (opts.autoRegen === 'will-run') {
|
|
63
|
+
lines.push('Auto-regeneration will run before the release is tagged.');
|
|
64
|
+
} else if (opts.autoRegen === 'deferred') {
|
|
65
|
+
lines.push('Library will be auto-regenerated when you run create-pr.');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return lines.join('\n');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Render a Markdown section for the PR body when Library is stale.
|
|
73
|
+
*
|
|
74
|
+
* The section heading is exactly `## ⚠️ Library is stale` — do not
|
|
75
|
+
* change this; downstream tooling may key on it. Includes stale-books
|
|
76
|
+
* list with reasons and a remediation section that varies by
|
|
77
|
+
* `opts.autoRegen`:
|
|
78
|
+
* - `'completed'` — auto-regen ran successfully; no scripts listed
|
|
79
|
+
* - `'failed'` — auto-regen failed; scripts listed as fallback
|
|
80
|
+
*
|
|
81
|
+
* The acknowledgement line is always present.
|
|
82
|
+
*
|
|
83
|
+
* Returns an empty string when the Library is clean.
|
|
84
|
+
*
|
|
85
|
+
* @param {Object} verifyResult
|
|
86
|
+
* @param {boolean} verifyResult.clean
|
|
87
|
+
* @param {Array<{ path: string, reasons: string[] }>} verifyResult.staleBooks
|
|
88
|
+
* @param {string[]} verifyResult.recommendedScripts
|
|
89
|
+
* @param {Object} opts
|
|
90
|
+
* @param {'completed'|'failed'} opts.autoRegen — controls the remediation section
|
|
91
|
+
* @returns {string} Markdown section, or empty string if clean
|
|
92
|
+
*/
|
|
93
|
+
export function PR_BODY_SECTION_TEMPLATE(verifyResult, opts = {}) {
|
|
94
|
+
if (verifyResult.clean) {
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const lines = [];
|
|
99
|
+
|
|
100
|
+
lines.push('## ⚠️ Library is stale');
|
|
101
|
+
lines.push('');
|
|
102
|
+
lines.push('The following Library books are out of date:');
|
|
103
|
+
lines.push('');
|
|
104
|
+
|
|
105
|
+
for (const book of verifyResult.staleBooks) {
|
|
106
|
+
const reasonText = book.reasons.join('; ');
|
|
107
|
+
lines.push(`- \`${book.path}\` — ${reasonText}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
lines.push('');
|
|
111
|
+
|
|
112
|
+
if (opts.autoRegen === 'completed') {
|
|
113
|
+
lines.push('**Auto-regeneration was performed.** The books listed above were regenerated in the release commit.');
|
|
114
|
+
} else if (opts.autoRegen === 'failed') {
|
|
115
|
+
lines.push('**Recommended remediation scripts:**');
|
|
116
|
+
lines.push('');
|
|
117
|
+
for (const script of verifyResult.recommendedScripts) {
|
|
118
|
+
lines.push(`- \`${script}\``);
|
|
119
|
+
}
|
|
120
|
+
lines.push('');
|
|
121
|
+
lines.push('_Auto-regeneration was attempted but failed. Run the scripts manually._');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
lines.push('');
|
|
125
|
+
lines.push(
|
|
126
|
+
'_This release was tagged via claude-hooks. The Library lifecycle pipeline is integrated with `create-pr` and `back-merge` — staleness at release time indicates a path that bypassed the pipeline._'
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
return lines.join('\n');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Tag value used to mark a PR or Linear issue as library-stale.
|
|
134
|
+
*
|
|
135
|
+
* Applied as a Linear label slug or a PR-title prefix by consumers.
|
|
136
|
+
*
|
|
137
|
+
* @type {string}
|
|
138
|
+
*/
|
|
139
|
+
export const PR_TAG_VALUE = 'library-stale';
|
|
22
140
|
|
|
23
141
|
export const LIBRARY_VERIFY_SKIPPED_WARNING =
|
|
24
142
|
'Library verification skipped due to an unexpected error. ' +
|
|
@@ -502,10 +502,11 @@ async function verifySDKConnection() {
|
|
|
502
502
|
* @param {number} options.timeout - Timeout in milliseconds (default: 120000 = 2 minutes)
|
|
503
503
|
* @param {string} options.model - Claude model override (e.g., 'haiku', 'sonnet', 'opus')
|
|
504
504
|
* @param {boolean} options.headless - Use SDK instead of CLI (default: false)
|
|
505
|
+
* @param {boolean} options.print - Use CLI print mode (-p): text-only output, no tool use (default: false)
|
|
505
506
|
* @returns {Promise<string>} Claude's response
|
|
506
507
|
* @throws {ClaudeClientError} If execution fails or times out
|
|
507
508
|
*/
|
|
508
|
-
const executeClaude = (prompt, { timeout = 120000, allowedTools = [], model = null, headless = false, maxTokens = null, costTracker = null } = {}) => {
|
|
509
|
+
const executeClaude = (prompt, { timeout = 120000, allowedTools = [], model = null, headless = false, maxTokens = null, costTracker = null, print = false } = {}) => {
|
|
509
510
|
// Headless mode: use Anthropic SDK directly (GH#133)
|
|
510
511
|
// Branch here (not in executeClaudeWithRetry) because analyzeCode calls
|
|
511
512
|
// executeClaude directly via withRetry — branching here covers all paths.
|
|
@@ -548,10 +549,14 @@ const executeClaude = (prompt, { timeout = 120000, allowedTools = [], model = nu
|
|
|
548
549
|
// executed inside bash --login so that .profile/.bashrc set up the correct PATH.
|
|
549
550
|
// All CLI flags are embedded in the bash -c command string (no spaces in flag values).
|
|
550
551
|
const claudeParts = [loginShell.path];
|
|
552
|
+
if (print) claudeParts.push('-p');
|
|
551
553
|
if (allowedTools.length > 0) claudeParts.push(`--allowedTools ${allowedTools.join(',')}`);
|
|
552
554
|
if (model) claudeParts.push(`--model ${model}`);
|
|
553
555
|
finalArgs.push('bash', '-lc', claudeParts.join(' '));
|
|
554
556
|
} else {
|
|
557
|
+
if (print) {
|
|
558
|
+
finalArgs.push('-p');
|
|
559
|
+
}
|
|
555
560
|
if (allowedTools.length > 0) {
|
|
556
561
|
// Format: --allowedTools "mcp__github__create_pull_request,mcp__github__get_file_contents"
|
|
557
562
|
finalArgs.push('--allowedTools', allowedTools.join(','));
|
|
@@ -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/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);
|
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-git-hooks",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.66.1",
|
|
4
4
|
"description": "Git hooks with Claude CLI for code analysis and automatic commit messages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
"test:changed": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --changedSince=main --forceExit",
|
|
17
17
|
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
|
|
18
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",
|
|
19
21
|
"lint": "eslint lib/ bin/claude-hooks .library/librarian/",
|
|
20
22
|
"lint:fix": "eslint lib/ bin/claude-hooks .library/librarian/ --fix",
|
|
21
23
|
"format": "prettier --write \"lib/**/*.js\" \"bin/**\" \"test/**/*.js\"",
|