claude-git-hooks 2.18.0 → 2.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/CLAUDE.md +12 -8
  3. package/README.md +2 -1
  4. package/bin/claude-hooks +75 -89
  5. package/lib/cli-metadata.js +301 -0
  6. package/lib/commands/analyze-diff.js +12 -10
  7. package/lib/commands/analyze.js +9 -5
  8. package/lib/commands/bump-version.js +66 -43
  9. package/lib/commands/create-pr.js +71 -34
  10. package/lib/commands/debug.js +4 -7
  11. package/lib/commands/generate-changelog.js +11 -4
  12. package/lib/commands/help.js +47 -27
  13. package/lib/commands/helpers.js +66 -43
  14. package/lib/commands/hooks.js +15 -13
  15. package/lib/commands/install.js +546 -39
  16. package/lib/commands/migrate-config.js +8 -11
  17. package/lib/commands/presets.js +6 -13
  18. package/lib/commands/setup-github.js +12 -3
  19. package/lib/commands/telemetry-cmd.js +8 -6
  20. package/lib/commands/update.js +1 -2
  21. package/lib/config.js +36 -31
  22. package/lib/hooks/pre-commit.js +34 -54
  23. package/lib/hooks/prepare-commit-msg.js +39 -58
  24. package/lib/utils/analysis-engine.js +28 -21
  25. package/lib/utils/changelog-generator.js +162 -34
  26. package/lib/utils/claude-client.js +438 -377
  27. package/lib/utils/claude-diagnostics.js +20 -10
  28. package/lib/utils/file-operations.js +51 -79
  29. package/lib/utils/file-utils.js +46 -9
  30. package/lib/utils/git-operations.js +140 -123
  31. package/lib/utils/git-tag-manager.js +24 -23
  32. package/lib/utils/github-api.js +85 -61
  33. package/lib/utils/github-client.js +12 -14
  34. package/lib/utils/installation-diagnostics.js +4 -4
  35. package/lib/utils/interactive-ui.js +29 -17
  36. package/lib/utils/logger.js +4 -1
  37. package/lib/utils/pr-metadata-engine.js +67 -33
  38. package/lib/utils/preset-loader.js +20 -62
  39. package/lib/utils/prompt-builder.js +50 -55
  40. package/lib/utils/resolution-prompt.js +33 -44
  41. package/lib/utils/sanitize.js +20 -19
  42. package/lib/utils/task-id.js +27 -40
  43. package/lib/utils/telemetry.js +29 -17
  44. package/lib/utils/version-manager.js +173 -126
  45. package/lib/utils/which-command.js +23 -12
  46. package/package.json +69 -69
@@ -76,8 +76,12 @@ export const detectClaudeError = (stdout = '', stderr = '', exitCode = 1) => {
76
76
  }
77
77
 
78
78
  // 3. Authentication failure detection
79
- if (stdout.includes('not authenticated') || stderr.includes('not authenticated') ||
80
- stdout.includes('authentication failed') || stderr.includes('authentication failed')) {
79
+ if (
80
+ stdout.includes('not authenticated') ||
81
+ stderr.includes('not authenticated') ||
82
+ stdout.includes('authentication failed') ||
83
+ stderr.includes('authentication failed')
84
+ ) {
81
85
  return {
82
86
  type: ClaudeErrorType.AUTH_FAILED,
83
87
  exitCode
@@ -85,8 +89,12 @@ export const detectClaudeError = (stdout = '', stderr = '', exitCode = 1) => {
85
89
  }
86
90
 
87
91
  // 4. Network errors
88
- if (stderr.includes('ENOTFOUND') || stderr.includes('ECONNREFUSED') ||
89
- stderr.includes('network error') || stderr.includes('connection refused')) {
92
+ if (
93
+ stderr.includes('ENOTFOUND') ||
94
+ stderr.includes('ECONNREFUSED') ||
95
+ stderr.includes('network error') ||
96
+ stderr.includes('connection refused')
97
+ ) {
90
98
  return {
91
99
  type: ClaudeErrorType.NETWORK,
92
100
  exitCode
@@ -150,7 +158,6 @@ const formatTimingInfo = (errorInfo) => {
150
158
  * @returns {string} Formatted error message
151
159
  */
152
160
  export const formatClaudeError = (errorInfo) => {
153
-
154
161
  switch (errorInfo.type) {
155
162
  case ClaudeErrorType.EXECUTION_ERROR:
156
163
  return formatExecutionError(errorInfo);
@@ -219,7 +226,9 @@ const formatRateLimitError = (errorInfo) => {
219
226
  const hours = Math.ceil(minutesUntilReset / 60);
220
227
  lines.push(` Time until reset: ~${hours} hour${hours > 1 ? 's' : ''}`);
221
228
  } else if (minutesUntilReset > 0) {
222
- lines.push(` Time until reset: ~${minutesUntilReset} minute${minutesUntilReset !== 1 ? 's' : ''}`);
229
+ lines.push(
230
+ ` Time until reset: ~${minutesUntilReset} minute${minutesUntilReset !== 1 ? 's' : ''}`
231
+ );
223
232
  } else {
224
233
  lines.push(' Limit should be reset now');
225
234
  }
@@ -345,7 +354,8 @@ const formatGenericError = (errorInfo) => {
345
354
  * @param {Object} errorInfo - Output from detectClaudeError()
346
355
  * @returns {boolean} True if error might resolve with retry
347
356
  */
348
- export const isRecoverableError = (errorInfo) => errorInfo.type === ClaudeErrorType.EXECUTION_ERROR ||
349
- errorInfo.type === ClaudeErrorType.RATE_LIMIT ||
350
- errorInfo.type === ClaudeErrorType.NETWORK ||
351
- errorInfo.type === ClaudeErrorType.TIMEOUT;
357
+ export const isRecoverableError = (errorInfo) =>
358
+ errorInfo.type === ClaudeErrorType.EXECUTION_ERROR ||
359
+ errorInfo.type === ClaudeErrorType.RATE_LIMIT ||
360
+ errorInfo.type === ClaudeErrorType.NETWORK ||
361
+ errorInfo.type === ClaudeErrorType.TIMEOUT;
@@ -78,11 +78,7 @@ const fileExists = async (filePath) => {
78
78
  * @throws {FileOperationError} If file too large or read fails
79
79
  */
80
80
  const readFile = async (filePath, { maxSize = 100000, encoding = 'utf8' } = {}) => {
81
- logger.debug(
82
- 'file-operations - readFile',
83
- 'Reading file',
84
- { filePath, maxSize, encoding }
85
- );
81
+ logger.debug('file-operations - readFile', 'Reading file', { filePath, maxSize, encoding });
86
82
 
87
83
  try {
88
84
  const size = await getFileSize(filePath);
@@ -97,14 +93,12 @@ const readFile = async (filePath, { maxSize = 100000, encoding = 'utf8' } = {})
97
93
 
98
94
  const content = await fs.readFile(filePath, encoding);
99
95
 
100
- logger.debug(
101
- 'file-operations - readFile',
102
- 'File read successfully',
103
- { filePath, contentLength: content.length }
104
- );
96
+ logger.debug('file-operations - readFile', 'File read successfully', {
97
+ filePath,
98
+ contentLength: content.length
99
+ });
105
100
 
106
101
  return content;
107
-
108
102
  } catch (error) {
109
103
  if (error instanceof FileOperationError) {
110
104
  throw error;
@@ -132,15 +126,14 @@ const hasAllowedExtension = (filePath, allowedExtensions = []) => {
132
126
  }
133
127
 
134
128
  const ext = path.extname(filePath).toLowerCase();
135
- const isAllowed = allowedExtensions.some(allowed =>
136
- ext === allowed.toLowerCase()
137
- );
129
+ const isAllowed = allowedExtensions.some((allowed) => ext === allowed.toLowerCase());
138
130
 
139
- logger.debug(
140
- 'file-operations - hasAllowedExtension',
141
- 'Extension check',
142
- { filePath, ext, allowedExtensions, isAllowed }
143
- );
131
+ logger.debug('file-operations - hasAllowedExtension', 'Extension check', {
132
+ filePath,
133
+ ext,
134
+ allowedExtensions,
135
+ isAllowed
136
+ });
144
137
 
145
138
  return isAllowed;
146
139
  };
@@ -166,27 +159,19 @@ const hasAllowedExtension = (filePath, allowedExtensions = []) => {
166
159
  * ]
167
160
  */
168
161
  const filterFiles = async (files, { maxSize = 100000, extensions = [] } = {}) => {
169
- logger.debug(
170
- 'file-operations - filterFiles',
171
- 'Filtering files',
172
- {
173
- fileCount: files.length,
174
- maxSize,
175
- extensions,
176
- 'process.cwd()': process.cwd(),
177
- files
178
- }
179
- );
162
+ logger.debug('file-operations - filterFiles', 'Filtering files', {
163
+ fileCount: files.length,
164
+ maxSize,
165
+ extensions,
166
+ 'process.cwd()': process.cwd(),
167
+ files
168
+ });
180
169
 
181
170
  const results = await Promise.allSettled(
182
171
  files.map(async (filePath) => {
183
172
  // Check extension first (fast)
184
173
  if (!hasAllowedExtension(filePath, extensions)) {
185
- logger.debug(
186
- 'file-operations - filterFiles',
187
- 'Extension rejected',
188
- { filePath }
189
- );
174
+ logger.debug('file-operations - filterFiles', 'Extension rejected', { filePath });
190
175
  return {
191
176
  path: filePath,
192
177
  size: 0,
@@ -197,11 +182,10 @@ const filterFiles = async (files, { maxSize = 100000, extensions = [] } = {}) =>
197
182
 
198
183
  // Check if file exists
199
184
  const exists = await fileExists(filePath);
200
- logger.debug(
201
- 'file-operations - filterFiles',
202
- 'File exists check',
203
- { filePath, exists }
204
- );
185
+ logger.debug('file-operations - filterFiles', 'File exists check', {
186
+ filePath,
187
+ exists
188
+ });
205
189
  if (!exists) {
206
190
  return {
207
191
  path: filePath,
@@ -216,11 +200,13 @@ const filterFiles = async (files, { maxSize = 100000, extensions = [] } = {}) =>
216
200
  const size = await getFileSize(filePath);
217
201
 
218
202
  if (size > maxSize) {
219
- logger.debug(
220
- 'file-operations - filterFiles',
221
- 'File too large',
222
- { filePath, size, maxSize, 'size (KB)': Math.round(size / 1024), 'maxSize (KB)': Math.round(maxSize / 1024) }
223
- );
203
+ logger.debug('file-operations - filterFiles', 'File too large', {
204
+ filePath,
205
+ size,
206
+ maxSize,
207
+ 'size (KB)': Math.round(size / 1024),
208
+ 'maxSize (KB)': Math.round(maxSize / 1024)
209
+ });
224
210
  return {
225
211
  path: filePath,
226
212
  size,
@@ -229,11 +215,12 @@ const filterFiles = async (files, { maxSize = 100000, extensions = [] } = {}) =>
229
215
  };
230
216
  }
231
217
 
232
- logger.debug(
233
- 'file-operations - filterFiles',
234
- 'File passed size check',
235
- { filePath, size, maxSize, 'size (KB)': Math.round(size / 1024) }
236
- );
218
+ logger.debug('file-operations - filterFiles', 'File passed size check', {
219
+ filePath,
220
+ size,
221
+ maxSize,
222
+ 'size (KB)': Math.round(size / 1024)
223
+ });
237
224
 
238
225
  return {
239
226
  path: filePath,
@@ -241,13 +228,11 @@ const filterFiles = async (files, { maxSize = 100000, extensions = [] } = {}) =>
241
228
  valid: true,
242
229
  reason: null
243
230
  };
244
-
245
231
  } catch (error) {
246
- logger.debug(
247
- 'file-operations - filterFiles',
248
- 'Error reading file',
249
- { filePath, error: error.message }
250
- );
232
+ logger.debug('file-operations - filterFiles', 'Error reading file', {
233
+ filePath,
234
+ error: error.message
235
+ });
251
236
  return {
252
237
  path: filePath,
253
238
  size: 0,
@@ -259,32 +244,19 @@ const filterFiles = async (files, { maxSize = 100000, extensions = [] } = {}) =>
259
244
  );
260
245
 
261
246
  // Extract successful results
262
- const fileMetadata = results
263
- .filter(r => r.status === 'fulfilled')
264
- .map(r => r.value);
247
+ const fileMetadata = results.filter((r) => r.status === 'fulfilled').map((r) => r.value);
265
248
 
266
- const validFiles = fileMetadata.filter(f => f.valid);
267
- const invalidFiles = fileMetadata.filter(f => !f.valid);
249
+ const validFiles = fileMetadata.filter((f) => f.valid);
250
+ const invalidFiles = fileMetadata.filter((f) => !f.valid);
268
251
 
269
- logger.debug(
270
- 'file-operations - filterFiles',
271
- 'Filtering complete',
272
- {
273
- totalFiles: files.length,
274
- validFiles: validFiles.length,
275
- invalidFiles: invalidFiles.length,
276
- rejectedFiles: invalidFiles.map(f => ({ path: f.path, reason: f.reason }))
277
- }
278
- );
252
+ logger.debug('file-operations - filterFiles', 'Filtering complete', {
253
+ totalFiles: files.length,
254
+ validFiles: validFiles.length,
255
+ invalidFiles: invalidFiles.length,
256
+ rejectedFiles: invalidFiles.map((f) => ({ path: f.path, reason: f.reason }))
257
+ });
279
258
 
280
259
  return fileMetadata;
281
260
  };
282
261
 
283
- export {
284
- FileOperationError,
285
- getFileSize,
286
- fileExists,
287
- readFile,
288
- hasAllowedExtension,
289
- filterFiles
290
- };
262
+ export { FileOperationError, getFileSize, fileExists, readFile, hasAllowedExtension, filterFiles };
@@ -3,7 +3,8 @@
3
3
  * Purpose: Utility functions for file system operations
4
4
  */
5
5
 
6
- import fs from 'fs/promises';
6
+ import fsPromises from 'fs/promises';
7
+ import fs from 'fs';
7
8
  import path from 'path';
8
9
  import { getRepoRoot } from './git-operations.js';
9
10
  import logger from './logger.js';
@@ -15,12 +16,10 @@ import logger from './logger.js';
15
16
  * @returns {Promise<void>}
16
17
  */
17
18
  export const ensureDir = async (dirPath) => {
18
- const absolutePath = path.isAbsolute(dirPath)
19
- ? dirPath
20
- : path.join(getRepoRoot(), dirPath);
19
+ const absolutePath = path.isAbsolute(dirPath) ? dirPath : path.join(getRepoRoot(), dirPath);
21
20
 
22
21
  try {
23
- await fs.mkdir(absolutePath, { recursive: true });
22
+ await fsPromises.mkdir(absolutePath, { recursive: true });
24
23
  logger.debug('file-utils - ensureDir', 'Directory ensured', { path: absolutePath });
25
24
  } catch (error) {
26
25
  logger.error('file-utils - ensureDir', 'Failed to create directory', error);
@@ -51,14 +50,52 @@ export const ensureOutputDir = async (config) => {
51
50
  export const writeOutputFile = async (filePath, content, config) => {
52
51
  await ensureOutputDir(config);
53
52
 
54
- const absolutePath = path.isAbsolute(filePath)
55
- ? filePath
56
- : path.join(getRepoRoot(), filePath);
53
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(getRepoRoot(), filePath);
57
54
 
58
- await fs.writeFile(absolutePath, content, 'utf8');
55
+ await fsPromises.writeFile(absolutePath, content, 'utf8');
59
56
 
60
57
  logger.debug('file-utils - writeOutputFile', 'File written', {
61
58
  path: absolutePath,
62
59
  size: content.length
63
60
  });
64
61
  };
62
+
63
+ /**
64
+ * Walks a directory tree recursively, invoking callbacks for each file
65
+ * Why: Shared traversal logic for discoverVersionFiles and discoverChangelogFiles
66
+ *
67
+ * @param {string} rootDir - Starting directory path
68
+ * @param {Object} options
69
+ * @param {number} options.maxDepth - Maximum recursion depth (default: 3)
70
+ * @param {Set<string>} options.ignoreSet - Directory names to skip
71
+ * @param {Function} options.onFile - Called as onFile(entry, fullPath) for each file entry
72
+ * @param {Function} options.onError - Called as onError(dir, error) on readdirSync failures
73
+ */
74
+ export function walkDirectoryTree(
75
+ rootDir,
76
+ { maxDepth = 3, ignoreSet = new Set(), onFile, onError } = {}
77
+ ) {
78
+ function walk(dir, depth) {
79
+ if (depth > maxDepth) return;
80
+
81
+ try {
82
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
83
+
84
+ for (const entry of entries) {
85
+ if (entry.name.startsWith('.') || ignoreSet.has(entry.name)) continue;
86
+
87
+ const fullPath = path.join(dir, entry.name);
88
+
89
+ if (entry.isDirectory()) {
90
+ walk(fullPath, depth + 1);
91
+ } else if (entry.isFile()) {
92
+ onFile?.(entry, fullPath);
93
+ }
94
+ }
95
+ } catch (error) {
96
+ onError?.(dir, error);
97
+ }
98
+ }
99
+
100
+ walk(rootDir, 0);
101
+ }