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
@@ -14,11 +14,7 @@ import path from 'path';
14
14
  import { getPackageJson } from './helpers.js';
15
15
  import { executeClaudeWithRetry } from '../utils/claude-client.js';
16
16
  import { loadPrompt } from '../utils/prompt-builder.js';
17
- import {
18
- fetchFileContent,
19
- fetchDirectoryListing,
20
- createIssue
21
- } from '../utils/github-api.js';
17
+ import { fetchFileContent, fetchDirectoryListing, createIssue } from '../utils/github-api.js';
22
18
  import { promptMenu, promptEditField, promptConfirmation } from '../utils/interactive-ui.js';
23
19
  import logger from '../utils/logger.js';
24
20
 
@@ -147,7 +143,10 @@ const readClaudeMd = async () => {
147
143
  logger.debug('help - readClaudeMd', 'CLAUDE.md loaded', { length: content.length });
148
144
  return content;
149
145
  } catch (error) {
150
- logger.debug('help - readClaudeMd', 'CLAUDE.md not found', { path: claudeMdPath, error: error.message });
146
+ logger.debug('help - readClaudeMd', 'CLAUDE.md not found', {
147
+ path: claudeMdPath,
148
+ error: error.message
149
+ });
151
150
  return null;
152
151
  }
153
152
  };
@@ -170,7 +169,10 @@ async function runAiHelp(question) {
170
169
  try {
171
170
  const claudeMdContent = await readClaudeMd();
172
171
  if (!claudeMdContent) {
173
- logger.debug('help - runAiHelp', 'CLAUDE.md not available, falling back to static help');
172
+ logger.debug(
173
+ 'help - runAiHelp',
174
+ 'CLAUDE.md not available, falling back to static help'
175
+ );
174
176
  showStaticHelp();
175
177
  return;
176
178
  }
@@ -190,9 +192,15 @@ async function runAiHelp(question) {
190
192
  const trimmedResponse = response.trim();
191
193
 
192
194
  // Check for NEED_MORE_CONTEXT second pass
193
- const needMoreLine = trimmedResponse.split('\n').find(l => l.includes('NEED_MORE_CONTEXT'));
195
+ const needMoreLine = trimmedResponse
196
+ .split('\n')
197
+ .find((l) => l.includes('NEED_MORE_CONTEXT'));
194
198
  if (needMoreLine) {
195
- const enrichedResponse = await handleNeedMoreContext(needMoreLine, question, claudeMdContent);
199
+ const enrichedResponse = await handleNeedMoreContext(
200
+ needMoreLine,
201
+ question,
202
+ claudeMdContent
203
+ );
196
204
  if (enrichedResponse) {
197
205
  printAiResponse(enrichedResponse, `${localVersion} + source`);
198
206
  return;
@@ -200,7 +208,7 @@ async function runAiHelp(question) {
200
208
  // Enrichment failed: show first-pass answer with marker line stripped
201
209
  const cleanResponse = trimmedResponse
202
210
  .split('\n')
203
- .filter(l => !l.includes('NEED_MORE_CONTEXT'))
211
+ .filter((l) => !l.includes('NEED_MORE_CONTEXT'))
204
212
  .join('\n')
205
213
  .trim();
206
214
  if (cleanResponse) {
@@ -211,7 +219,9 @@ async function runAiHelp(question) {
211
219
 
212
220
  printAiResponse(trimmedResponse, localVersion);
213
221
  } catch (error) {
214
- logger.debug('help - runAiHelp', 'AI help failed, falling back to static help', { error: error.message });
222
+ logger.debug('help - runAiHelp', 'AI help failed, falling back to static help', {
223
+ error: error.message
224
+ });
215
225
  showStaticHelp();
216
226
  }
217
227
  }
@@ -228,7 +238,10 @@ async function handleNeedMoreContext(needMoreLine, question, claudeMdContent) {
228
238
  try {
229
239
  // Parse file paths from line: "NEED_MORE_CONTEXT: file1.js, file2.js"
230
240
  const pathsPart = needMoreLine.replace('NEED_MORE_CONTEXT', '').replace(':', '').trim();
231
- const filePaths = pathsPart.split(',').map(p => p.trim()).filter(Boolean);
241
+ const filePaths = pathsPart
242
+ .split(',')
243
+ .map((p) => p.trim())
244
+ .filter(Boolean);
232
245
 
233
246
  if (filePaths.length === 0) {
234
247
  logger.debug('help - handleNeedMoreContext', 'No file paths in NEED_MORE_CONTEXT');
@@ -249,8 +262,8 @@ async function handleNeedMoreContext(needMoreLine, question, claudeMdContent) {
249
262
 
250
263
  // Build enriched documentation
251
264
  const additionalContent = fetchResults
252
- .filter(r => r.content !== null)
253
- .map(r => `\n--- Source: ${r.filePath} ---\n${r.content}`)
265
+ .filter((r) => r.content !== null)
266
+ .map((r) => `\n--- Source: ${r.filePath} ---\n${r.content}`)
254
267
  .join('\n');
255
268
 
256
269
  if (!additionalContent) {
@@ -295,13 +308,15 @@ async function runReportIssue() {
295
308
  if (!templates || templates.length === 0) {
296
309
  logger.debug('help - runReportIssue', 'No issue templates found');
297
310
  console.log('\nNo issue templates found in .github/ISSUE_TEMPLATE/');
298
- console.log(`Create issues directly at: https://github.com/${owner}/${repo}/issues/new`);
311
+ console.log(
312
+ `Create issues directly at: https://github.com/${owner}/${repo}/issues/new`
313
+ );
299
314
  return;
300
315
  }
301
316
 
302
317
  // Filter to markdown/yaml template files
303
- const templateFiles = templates.filter(t =>
304
- t.name.endsWith('.md') || t.name.endsWith('.yml') || t.name.endsWith('.yaml')
318
+ const templateFiles = templates.filter(
319
+ (t) => t.name.endsWith('.md') || t.name.endsWith('.yml') || t.name.endsWith('.yaml')
305
320
  );
306
321
 
307
322
  if (templateFiles.length === 0) {
@@ -345,12 +360,16 @@ async function runReportIssue() {
345
360
  const jsonMatch = questionsResponse.match(/\[[\s\S]*\]/);
346
361
  questions = jsonMatch ? JSON.parse(jsonMatch[0]) : null;
347
362
  } catch (parseError) {
348
- logger.debug('help - runReportIssue', 'Failed to parse questions JSON', { error: parseError.message });
363
+ logger.debug('help - runReportIssue', 'Failed to parse questions JSON', {
364
+ error: parseError.message
365
+ });
349
366
  questions = null;
350
367
  }
351
368
 
352
369
  if (!questions || !Array.isArray(questions) || questions.length === 0) {
353
- console.log('\nCould not generate questions from template. Please create the issue manually:');
370
+ console.log(
371
+ '\nCould not generate questions from template. Please create the issue manually:'
372
+ );
354
373
  console.log(`https://github.com/${owner}/${repo}/issues/new`);
355
374
  return;
356
375
  }
@@ -359,10 +378,7 @@ async function runReportIssue() {
359
378
  console.log('\nPlease answer the following questions:\n');
360
379
  const answers = [];
361
380
  for (const q of questions) {
362
- const answer = await promptEditField(
363
- q.section,
364
- q.question
365
- );
381
+ const answer = await promptEditField(q.section, q.question);
366
382
  answers.push({ section: q.section, answer });
367
383
  }
368
384
 
@@ -370,7 +386,7 @@ async function runReportIssue() {
370
386
  const composePrompt = await loadPrompt('HELP_COMPOSE_ISSUE.md', {
371
387
  TEMPLATE_NAME: selectedTemplate.name,
372
388
  TEMPLATE_CONTENT: templateContent,
373
- USER_ANSWERS: answers.map(a => `${a.section}: ${a.answer}`).join('\n')
389
+ USER_ANSWERS: answers.map((a) => `${a.section}: ${a.answer}`).join('\n')
374
390
  });
375
391
 
376
392
  console.log('\nComposing issue...\n');
@@ -381,7 +397,9 @@ async function runReportIssue() {
381
397
  const jsonMatch = composeResponse.match(/\{[\s\S]*\}/);
382
398
  issueData = jsonMatch ? JSON.parse(jsonMatch[0]) : null;
383
399
  } catch (parseError) {
384
- logger.debug('help - runReportIssue', 'Failed to parse issue JSON', { error: parseError.message });
400
+ logger.debug('help - runReportIssue', 'Failed to parse issue JSON', {
401
+ error: parseError.message
402
+ });
385
403
  issueData = null;
386
404
  }
387
405
 
@@ -413,7 +431,9 @@ async function runReportIssue() {
413
431
  } catch (error) {
414
432
  logger.debug('help - runReportIssue', 'Report issue failed', { error: error.message });
415
433
  console.log(`\nCould not create issue: ${error.message}`);
416
- console.log('You can create issues manually at: https://github.com/mscope-S-L/git-hooks/issues/new');
434
+ console.log(
435
+ 'You can create issues manually at: https://github.com/mscope-S-L/git-hooks/issues/new'
436
+ );
417
437
  }
418
438
  }
419
439
 
@@ -434,6 +454,6 @@ function extractLabelsFromTemplate(templateContent) {
434
454
 
435
455
  return labelsMatch[1]
436
456
  .split(',')
437
- .map(l => l.trim().replace(/^["']|["']$/g, ''))
457
+ .map((l) => l.trim().replace(/^["']|["']$/g, ''))
438
458
  .filter(Boolean);
439
459
  }
@@ -71,7 +71,10 @@ export function checkGitRepo() {
71
71
  let gitdir = gitContent.substring(8).trim();
72
72
  // Convert Windows path to WSL if needed (C:\ -> /mnt/c/)
73
73
  if (/^[A-Za-z]:/.test(gitdir)) {
74
- gitdir = gitdir.replace(/^([A-Za-z]):/, (_, drive) => `/mnt/${drive.toLowerCase()}`);
74
+ gitdir = gitdir.replace(
75
+ /^([A-Za-z]):/,
76
+ (_, drive) => `/mnt/${drive.toLowerCase()}`
77
+ );
75
78
  gitdir = gitdir.replace(/\\/g, '/');
76
79
  }
77
80
  // Verify the gitdir exists
@@ -106,7 +109,10 @@ export function getGitHooksPath() {
106
109
  let gitCommonDir = execSync('git rev-parse --git-common-dir', { encoding: 'utf8' }).trim();
107
110
  // Handle Windows paths when running under WSL (e.g. C:\... -> /mnt/c/...)
108
111
  if (/^[A-Za-z]:/.test(gitCommonDir)) {
109
- gitCommonDir = gitCommonDir.replace(/^([A-Za-z]):/, (_, drive) => `/mnt/${drive.toLowerCase()}`);
112
+ gitCommonDir = gitCommonDir.replace(
113
+ /^([A-Za-z]):/,
114
+ (_, drive) => `/mnt/${drive.toLowerCase()}`
115
+ );
110
116
  gitCommonDir = gitCommonDir.replace(/\\/g, '/');
111
117
  }
112
118
  return path.join(gitCommonDir, 'hooks').replace(/\\/g, '/');
@@ -257,29 +263,33 @@ export async function updateConfig(propertyPath, value, options = {}) {
257
263
  export function getLatestVersion(packageName) {
258
264
  return new Promise((resolve, reject) => {
259
265
  // Use the main NPM API, not /latest
260
- https.get(`https://registry.npmjs.org/${packageName}`, (res) => {
261
- let data = '';
262
- res.on('data', chunk => data += chunk);
263
- res.on('end', () => {
264
- try {
265
- const json = JSON.parse(data);
266
- // Get the version from the 'latest' tag
267
- if (json['dist-tags'] && json['dist-tags'].latest) {
268
- resolve(json['dist-tags'].latest);
269
- } else {
270
- reject(new Error('Could not get the version'));
271
- }
272
- } catch (e) {
273
- // If it fails, try with npm view
266
+ https
267
+ .get(`https://registry.npmjs.org/${packageName}`, (res) => {
268
+ let data = '';
269
+ res.on('data', (chunk) => (data += chunk));
270
+ res.on('end', () => {
274
271
  try {
275
- const version = execSync(`npm view ${packageName} version`, { encoding: 'utf8' }).trim();
276
- resolve(version);
277
- } catch (npmError) {
278
- reject(e);
272
+ const json = JSON.parse(data);
273
+ // Get the version from the 'latest' tag
274
+ if (json['dist-tags'] && json['dist-tags'].latest) {
275
+ resolve(json['dist-tags'].latest);
276
+ } else {
277
+ reject(new Error('Could not get the version'));
278
+ }
279
+ } catch (e) {
280
+ // If it fails, try with npm view
281
+ try {
282
+ const version = execSync(`npm view ${packageName} version`, {
283
+ encoding: 'utf8'
284
+ }).trim();
285
+ resolve(version);
286
+ } catch (npmError) {
287
+ reject(e);
288
+ }
279
289
  }
280
- }
281
- });
282
- }).on('error', reject);
290
+ });
291
+ })
292
+ .on('error', reject);
283
293
  });
284
294
  }
285
295
 
@@ -301,14 +311,18 @@ export class Entertainment {
301
311
  static async getJoke() {
302
312
  return new Promise((resolve) => {
303
313
  // Try to get joke from API
304
- const req = https.get('https://icanhazdadjoke.com/', {
305
- headers: { 'Accept': 'text/plain' },
306
- timeout: 3000
307
- }, (res) => {
308
- let data = '';
309
- res.on('data', chunk => data += chunk);
310
- res.on('end', () => resolve(data.trim()));
311
- });
314
+ const req = https.get(
315
+ 'https://icanhazdadjoke.com/',
316
+ {
317
+ headers: { Accept: 'text/plain' },
318
+ timeout: 3000
319
+ },
320
+ (res) => {
321
+ let data = '';
322
+ res.on('data', (chunk) => (data += chunk));
323
+ res.on('end', () => resolve(data.trim()));
324
+ }
325
+ );
312
326
 
313
327
  req.on('error', () => {
314
328
  // If it fails, use local joke
@@ -332,9 +346,11 @@ export class Entertainment {
332
346
  let isFinished = false;
333
347
 
334
348
  // Get first joke from API without blocking
335
- this.getJoke().then(joke => {
336
- if (!isFinished) currentJoke = joke;
337
- }).catch(() => { }); // If it fails, keep the local one
349
+ this.getJoke()
350
+ .then((joke) => {
351
+ if (!isFinished) currentJoke = joke;
352
+ })
353
+ .catch(() => {}); // If it fails, keep the local one
338
354
 
339
355
  // Hide cursor
340
356
  process.stdout.write('\x1B[?25l');
@@ -356,13 +372,16 @@ export class Entertainment {
356
372
 
357
373
  // Refresh joke every 10 seconds
358
374
  if (jokeCountdown <= 0) {
359
- this.getJoke().then(joke => {
360
- if (!isFinished) currentJoke = joke;
361
- }).catch(() => {
362
- if (!isFinished) {
363
- currentJoke = this.jokes[Math.floor(Math.random() * this.jokes.length)];
364
- }
365
- });
375
+ this.getJoke()
376
+ .then((joke) => {
377
+ if (!isFinished) currentJoke = joke;
378
+ })
379
+ .catch(() => {
380
+ if (!isFinished) {
381
+ currentJoke =
382
+ this.jokes[Math.floor(Math.random() * this.jokes.length)];
383
+ }
384
+ });
366
385
  jokeCountdown = 10;
367
386
  }
368
387
  }
@@ -374,13 +393,17 @@ export class Entertainment {
374
393
  const spinner = spinners[spinnerIndex % spinners.length];
375
394
 
376
395
  // Line 1: Spinner
377
- process.stdout.write('\r\x1B[2K' + `${colors.yellow}${spinner} ${message}${colors.reset}\n`);
396
+ process.stdout.write(
397
+ '\r\x1B[2K' + `${colors.yellow}${spinner} ${message}${colors.reset}\n`
398
+ );
378
399
 
379
400
  // Line 2: Joke
380
401
  process.stdout.write('\r\x1B[2K' + `${colors.green}🎭 ${currentJoke}${colors.reset}\n`);
381
402
 
382
403
  // Line 3: Countdown
383
- process.stdout.write('\r\x1B[2K' + `${colors.yellow}⏱️ Next joke in: ${jokeCountdown}s${colors.reset}\n`);
404
+ process.stdout.write(
405
+ '\r\x1B[2K' + `${colors.yellow}⏱️ Next joke in: ${jokeCountdown}s${colors.reset}\n`
406
+ );
384
407
  }, 100);
385
408
 
386
409
  try {
@@ -5,14 +5,8 @@
5
5
 
6
6
  import fs from 'fs';
7
7
  import path from 'path';
8
- import {
9
- error,
10
- success,
11
- info,
12
- warning,
13
- checkGitRepo,
14
- getGitHooksPath
15
- } from './helpers.js';
8
+ import { error, success, info, warning, checkGitRepo, getGitHooksPath } from './helpers.js';
9
+ import { removeCompletions } from './install.js';
16
10
 
17
11
  /**
18
12
  * Enable command
@@ -26,7 +20,7 @@ export function runEnable(hookName) {
26
20
  const hooksDir = getGitHooksPath();
27
21
  const hooks = hookName ? [hookName] : ['pre-commit', 'prepare-commit-msg'];
28
22
 
29
- hooks.forEach(hook => {
23
+ hooks.forEach((hook) => {
30
24
  const disabledPath = `${hooksDir}/${hook}.disabled`;
31
25
  const enabledPath = `${hooksDir}/${hook}`;
32
26
 
@@ -53,7 +47,7 @@ export function runDisable(hookName) {
53
47
  const hooksDir = getGitHooksPath();
54
48
  const hooks = hookName ? [hookName] : ['pre-commit', 'prepare-commit-msg'];
55
49
 
56
- hooks.forEach(hook => {
50
+ hooks.forEach((hook) => {
57
51
  const enabledPath = `${hooksDir}/${hook}`;
58
52
  const disabledPath = `${hooksDir}/${hook}.disabled`;
59
53
 
@@ -80,7 +74,7 @@ export function runStatus() {
80
74
 
81
75
  const hooksDir = getGitHooksPath();
82
76
  const hooks = ['pre-commit', 'prepare-commit-msg'];
83
- hooks.forEach(hook => {
77
+ hooks.forEach((hook) => {
84
78
  const enabledPath = `${hooksDir}/${hook}`;
85
79
  const disabledPath = `${hooksDir}/${hook}.disabled`;
86
80
 
@@ -96,7 +90,7 @@ export function runStatus() {
96
90
  // Check guidelines files
97
91
  console.log('\nGuidelines files:');
98
92
  const guidelines = ['CLAUDE_PRE_COMMIT.md'];
99
- guidelines.forEach(guideline => {
93
+ guidelines.forEach((guideline) => {
100
94
  const promptsPath = path.join('.claude', 'prompts', guideline);
101
95
  const legacyPath = path.join('.claude', guideline);
102
96
  if (fs.existsSync(promptsPath)) {
@@ -142,7 +136,7 @@ export function runUninstall() {
142
136
  const hooksPath = getGitHooksPath();
143
137
  const hooks = ['pre-commit', 'prepare-commit-msg'];
144
138
 
145
- hooks.forEach(hook => {
139
+ hooks.forEach((hook) => {
146
140
  const hookPath = `${hooksPath}/${hook}`;
147
141
  if (fs.existsSync(hookPath)) {
148
142
  fs.unlinkSync(hookPath);
@@ -150,5 +144,13 @@ export function runUninstall() {
150
144
  }
151
145
  });
152
146
 
147
+ // Remove shell completion scripts and rc modifications
148
+ try {
149
+ removeCompletions();
150
+ success('Shell completions removed');
151
+ } catch (e) {
152
+ warning(`Could not remove shell completions: ${e.message}`);
153
+ }
154
+
153
155
  success('Claude Git Hooks uninstalled');
154
156
  }