claude-git-hooks 2.13.0 → 2.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,44 @@ 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.14.1] - 2026-02-06
9
+
10
+ ### 🔧 Changed
11
+ - Improved `bump-version` command workflow - now automatically stages and commits version changes with conventional commit format before tag creation
12
+ - Enhanced `bump-version` command flexibility - added `--no-commit` flag for manual workflows where users prefer to commit changes themselves
13
+ - Improved error handling in `bump-version` - provides clearer guidance when staging or committing fails, with manual fallback instructions
14
+ - Enhanced next-steps guidance in `bump-version` output - adapts instructions based on flags used (--no-commit, --no-push, --no-tag)
15
+
16
+ ### 🐛 Fixed
17
+ - Fixed `bump-version` workflow inconsistency - prevented tag creation when using `--no-commit` flag, as tags should only be created after changes are committed
18
+ - Fixed potential git history issues - version bump commits now use `--no-verify` flag to bypass pre-commit hooks and prevent circular execution
19
+
20
+ ### 🗑️ Removed
21
+ - Removed redundant fallback warnings from `analyze-diff` and `create-pr` commands - warnings were unnecessary as base branch resolution now throws clear errors with suggestions instead
22
+
23
+
24
+ ## [2.14.0] - 2026-02-06
25
+
26
+ ### ✨ Added
27
+ - PR metadata engine (`pr-metadata-engine.js`) - centralized module for generating PR titles, descriptions, and test plans from branch diffs with timeout resilience (#63)
28
+ - Tiered diff reduction system - automatically truncates large diffs using context reduction, proportional budgets, and stat-only summaries to prevent timeouts
29
+ - Extended git operations - added `fetchRemote()`, `branchExists()`, `resolveBaseBranch()`, `getChangedFilesBetweenRefs()`, `getDiffBetweenRefs()`, and `getCommitsBetweenRefs()` to `git-operations.js`
30
+ - RFC-001 documentation - comprehensive design specification for PR metadata engine refactor with timeout resilience strategy
31
+
32
+ ### 🔧 Changed
33
+ - Refactored `analyze-diff` command to use shared PR metadata engine - reduced from 262 to ~80 lines by eliminating duplicate git logic
34
+ - Refactored `create-pr` command to use shared PR metadata engine - eliminated duplicate diff extraction logic (lines 340-420)
35
+ - Improved timeout error handling - timeout errors now include `errorInfo` and are classified as recoverable for automatic retry with exponential backoff
36
+ - Updated CLAUDE.md with PR metadata engine architecture, new git operations exports, and tiered diff reduction strategy
37
+
38
+ ### 🐛 Fixed
39
+ - Fixed timeout failures in `analyze-diff` for large diffs - now uses tiered reduction instead of monolithic diff processing
40
+ - Fixed inconsistent behavior between `analyze-diff` and `create-pr` commands - both now share identical analysis logic via unified engine
41
+
42
+ ### 🗑️ Removed
43
+ - Removed unreliable `SUBAGENT_INSTRUCTION` text hint - replaced with deterministic tiered diff reduction for parallel processing
44
+
45
+
8
46
  ## [2.13.0] - 2026-02-05
9
47
 
10
48
  ### ✨ Added
package/README.md CHANGED
@@ -110,12 +110,16 @@ claude-hooks bump-version major --update-changelog
110
110
 
111
111
  # Preview without applying
112
112
  claude-hooks bump-version patch --dry-run
113
+
114
+ # Manual workflow (skip automatic commit)
115
+ claude-hooks bump-version patch --no-commit
113
116
  ```
114
117
 
115
118
  **What it does:**
116
119
  - Detects project type (Node.js, Maven, or monorepo with both)
117
120
  - Updates `package.json` and/or `pom.xml`
118
121
  - Generates CHANGELOG entry with Claude (analyzes commits)
122
+ - Commits changes automatically with conventional commit format
119
123
  - Creates annotated Git tag with `v` prefix (e.g., `v2.7.0`)
120
124
  - Pushes tag to remote automatically
121
125
 
@@ -253,7 +257,8 @@ claude-hooks --help # Full command reference
253
257
  | `analysis-engine.js` | **Shared analysis logic** - file data, orchestration, results (v2.13.0) | `buildFilesData()`, `runAnalysis()`, `consolidateResults()`, `displayResults()` |
254
258
  | `claude-client.js` | **Claude CLI wrapper** - spawn, retry, parallel execution | `analyzeCode()`, `analyzeCodeParallel()`, `executeClaudeWithRetry()` |
255
259
  | `prompt-builder.js` | **Prompt construction** - load templates, replace variables | `buildAnalysisPrompt()`, `loadPrompt()` |
256
- | `git-operations.js` | **Git abstractions** - staged files, diff, repo root | `getStagedFiles()`, `getDiff()`, `getRepoRoot()` |
260
+ | `git-operations.js` | **Git abstractions** - staged files, diff, branch comparison | `getStagedFiles()`, `getDiff()`, `getRepoRoot()`, `resolveBaseBranch()`, `getDiffBetweenRefs()` |
261
+ | `pr-metadata-engine.js` | **PR metadata generation** - branch context, diff reduction (v2.14.0) | `getBranchContext()`, `buildDiffPayload()`, `generatePRMetadata()`, `analyzeBranchForPR()` |
257
262
  | `github-api.js` | **Octokit integration** - PR creation, token validation | `createPullRequest()`, `validateToken()`, `saveGitHubToken()` |
258
263
  | `github-client.js` | **GitHub helpers** - CODEOWNERS parsing, reviewers | `getReviewersForFiles()`, `parseGitHubRepo()` |
259
264
  | `preset-loader.js` | **Preset system** - load tech-stack configurations | `loadPreset()`, `listPresets()` |
@@ -290,16 +295,18 @@ git commit -m "auto" → templates/prepare-commit-msg (bash wrapper)
290
295
  → write to COMMIT_EDITMSG
291
296
  ```
292
297
 
293
- #### PR Creation
298
+ #### PR Metadata Generation (analyze-diff / create-pr)
294
299
 
295
300
  ```
296
- claude-hooks create-pr → bin/claude-hooks (router)
297
- → lib/commands/create-pr.js
298
- validateToken() (github-api.js)
299
- parseGitHubRepo() (github-client.js)
300
- getReviewersForFiles() (CODEOWNERS + config)
301
- executeClaudeWithRetry() (metadata generation)
302
- createPullRequest() (Octokit API)
301
+ claude-hooks analyze-diff|create-pr → bin/claude-hooks (router)
302
+ → lib/commands/analyze-diff.js or create-pr.js (thin wrapper)
303
+ analyzeBranchForPR() (pr-metadata-engine.js)
304
+ resolveBaseBranch() (git-operations.js)
305
+ getDiffBetweenRefs() + getCommitsBetweenRefs()
306
+ buildDiffPayload() with tiered reduction (context → proportional → stat-only)
307
+ executeClaudeWithRetry() PRMetadata
308
+ → analyze-diff: formats to console + saves JSON
309
+ → create-pr: additionally creates PR via Octokit API
303
310
  ```
304
311
 
305
312
  ### Config Priority
@@ -3,15 +3,13 @@
3
3
  * Purpose: Analyze differences between branches and generate PR info
4
4
  */
5
5
 
6
- import { execSync } from 'child_process';
7
6
  import fs from 'fs';
8
- import { executeClaudeWithRetry, extractJSON } from '../utils/claude-client.js';
9
- import { loadPrompt } from '../utils/prompt-builder.js';
7
+ import { analyzeBranchForPR } from '../utils/pr-metadata-engine.js';
10
8
  import { getConfig } from '../config.js';
9
+ import logger from '../utils/logger.js';
11
10
  import {
12
11
  colors,
13
12
  error,
14
- success,
15
13
  info,
16
14
  warning,
17
15
  checkGitRepo
@@ -27,156 +25,38 @@ export async function runAnalyzeDiff(args) {
27
25
  return;
28
26
  }
29
27
 
30
- // Load configuration
28
+ // Enable debug mode from config
31
29
  const config = await getConfig();
32
-
33
- const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
34
-
35
- if (!currentBranch) {
36
- error('You are not in a valid branch.');
37
- return;
30
+ if (config.system?.debug) {
31
+ logger.setDebugMode(true);
38
32
  }
39
33
 
40
- // Update remote references
41
- execSync('git fetch', { stdio: 'ignore' });
42
-
43
- let baseBranch, compareWith, contextDescription;
44
-
45
- if (args[0]) {
46
- // Case with argument: compare current branch vs origin/specified-branch
47
- const targetBranch = args[0];
48
- baseBranch = `origin/${targetBranch}`;
49
- compareWith = `${baseBranch}...HEAD`;
50
- contextDescription = `${currentBranch} vs ${baseBranch}`;
51
-
52
- // Check that the origin branch exists
53
- try {
54
- execSync(`git rev-parse --verify ${baseBranch}`, { stdio: 'ignore' });
55
- } catch (e) {
56
- error(`Branch ${baseBranch} does not exist.`);
57
- return;
58
- }
59
- } else {
60
- // Case without argument: compare current branch vs origin/current-branch
61
- baseBranch = `origin/${currentBranch}`;
62
- compareWith = `${baseBranch}...HEAD`;
63
- contextDescription = `${currentBranch} vs ${baseBranch}`;
64
-
65
- // Check that the origin branch exists
66
- try {
67
- execSync(`git rev-parse --verify ${baseBranch}`, { stdio: 'ignore' });
68
- } catch (e) {
69
- // Try fallback to origin/develop
70
- baseBranch = 'origin/develop';
71
- compareWith = `${baseBranch}...HEAD`;
72
- contextDescription = `${currentBranch} vs ${baseBranch} (fallback)`;
73
-
74
- try {
75
- execSync(`git rev-parse --verify ${baseBranch}`, { stdio: 'ignore' });
76
- warning(`Branch origin/${currentBranch} does not exist. Using ${baseBranch} as fallback.`);
77
- } catch (e2) {
78
- // Try fallback to origin/main
79
- baseBranch = 'origin/main';
80
- compareWith = `${baseBranch}...HEAD`;
81
- contextDescription = `${currentBranch} vs ${baseBranch} (fallback)`;
34
+ // Parse target branch from arguments
35
+ const targetBranch = args[0];
82
36
 
83
- try {
84
- execSync(`git rev-parse --verify ${baseBranch}`, { stdio: 'ignore' });
85
- warning(`No origin/develop branch. Using ${baseBranch} as fallback.`);
86
- } catch (e3) {
87
- error('Could not find a valid comparison branch (tried origin/current, origin/develop, origin/main).');
88
- return;
89
- }
90
- }
91
- }
92
- }
93
-
94
- info(`Analyzing: ${contextDescription}...`);
37
+ info(targetBranch ? `Analyzing differences with ${targetBranch}...` : 'Analyzing differences...');
38
+ const startTime = Date.now();
95
39
 
96
- // Get modified files
97
- let diffFiles;
98
40
  try {
99
- diffFiles = execSync(`git diff ${compareWith} --name-only`, { encoding: 'utf8' }).trim();
100
-
101
- if (!diffFiles) {
102
- // Check if there are staged or unstaged changes
103
- const stagedFiles = execSync('git diff --cached --name-only', { encoding: 'utf8' }).trim();
104
- const unstagedFiles = execSync('git diff --name-only', { encoding: 'utf8' }).trim();
41
+ // Call PR metadata engine
42
+ const { success: engineSuccess, result, error: engineError } = await analyzeBranchForPR(targetBranch, {
43
+ hook: 'analyze-diff'
44
+ });
105
45
 
106
- if (stagedFiles || unstagedFiles) {
107
- warning('No differences with remote, but you have uncommitted local changes.');
108
- console.log('Staged changes:', stagedFiles || 'none');
109
- console.log('Unstaged changes:', unstagedFiles || 'none');
110
- } else {
111
- success('✅ No differences. Your branch is synchronized.');
112
- }
46
+ if (!engineSuccess) {
47
+ error(engineError || 'Failed to analyze branch');
113
48
  return;
114
49
  }
115
- } catch (e) {
116
- error('Error getting differences: ' + e.message);
117
- return;
118
- }
119
-
120
- // Get the complete diff
121
- let fullDiff, commits;
122
- try {
123
- fullDiff = execSync(`git diff ${compareWith}`, { encoding: 'utf8' });
124
- commits = execSync(`git log ${baseBranch}..HEAD --oneline`, { encoding: 'utf8' }).trim();
125
- } catch (e) {
126
- error('Error getting diff or commits: ' + e.message);
127
- return;
128
- }
129
-
130
- // Check if subagents should be used
131
- const useSubagents = config.subagents.enabled;
132
- const subagentModel = config.subagents.model;
133
- let subagentBatchSize = config.subagents.batchSize;
134
- // Validate batch size (must be >= 1)
135
- if (subagentBatchSize < 1) {
136
- subagentBatchSize = 1;
137
- }
138
- const subagentInstruction = useSubagents
139
- ? `\n\nIMPORTANT PARALLEL PROCESSING: If analyzing 3+ files, process them in batches of ${subagentBatchSize}. For EACH batch, create that many subagents in parallel using Task tool (send single message with multiple Task calls). Each subagent analyzes one file and provides insights. After ALL batches complete, consolidate into SINGLE JSON with ONE cohesive PR title/description. Model: ${subagentModel}. Example: 4 files with BATCH_SIZE=1 → 4 sequential batches of 1 subagent each. Example: 4 files with BATCH_SIZE=3 → batch 1 has 3 parallel subagents (files 1-3), batch 2 has 1 subagent (file 4).\n`
140
- : '';
141
-
142
- // Truncate full diff if too large
143
- const truncatedDiff = fullDiff.length > 50000
144
- ? fullDiff.substring(0, 50000) + '\n... (truncated diff)'
145
- : fullDiff;
146
50
 
147
- // Load prompt from template
148
- const prompt = await loadPrompt('ANALYZE_DIFF.md', {
149
- CONTEXT_DESCRIPTION: contextDescription,
150
- SUBAGENT_INSTRUCTION: subagentInstruction,
151
- COMMITS: commits,
152
- DIFF_FILES: diffFiles,
153
- FULL_DIFF: truncatedDiff
154
- });
155
-
156
- info('Sending to Claude for analysis...');
157
- const startTime = Date.now();
158
-
159
- // Prepare telemetry context
160
- const filesChanged = diffFiles.split('\n').length;
161
- const telemetryContext = {
162
- fileCount: filesChanged,
163
- batchSize: filesChanged,
164
- totalBatches: 1,
165
- model: subagentModel || 'sonnet',
166
- hook: 'analyze-diff'
167
- };
168
-
169
- try {
170
- // Use cross-platform executeClaudeWithRetry from claude-client.js with telemetry
171
- const response = await executeClaudeWithRetry(prompt, {
172
- timeout: 180000, // 3 minutes for diff analysis
173
- telemetryContext
174
- });
175
-
176
- // Extract JSON from response using claude-client utility
177
- const result = extractJSON(response);
51
+ // Log truncation details if applicable
52
+ if (result.context.isTruncated) {
53
+ logger.debug('analyze-diff', 'Diff was truncated', result.context.truncationDetails);
54
+ }
178
55
 
179
56
  // Show the results
57
+ const contextDescription = `${result.context.currentBranch} vs ${result.context.baseBranch}`;
58
+ const filesChanged = result.context.filesCount;
59
+
180
60
  console.log('');
181
61
  console.log('════════════════════════════════════════════════════════════════');
182
62
  console.log(' DIFFERENCES ANALYSIS ');
@@ -215,10 +95,15 @@ export async function runAnalyzeDiff(args) {
215
95
 
216
96
  // Save the results in a file with context
217
97
  const outputData = {
218
- ...result,
98
+ prTitle: result.prTitle,
99
+ prDescription: result.prDescription,
100
+ suggestedBranchName: result.suggestedBranchName,
101
+ changeType: result.changeType,
102
+ breakingChanges: result.breakingChanges,
103
+ testingNotes: result.testingNotes,
219
104
  context: {
220
- currentBranch,
221
- baseBranch,
105
+ currentBranch: result.context.currentBranch,
106
+ baseBranch: result.context.baseBranch,
222
107
  contextDescription,
223
108
  filesChanged,
224
109
  timestamp: new Date().toISOString()
@@ -243,13 +128,7 @@ export async function runAnalyzeDiff(args) {
243
128
 
244
129
  // Contextual suggestions
245
130
  console.log('');
246
- if (!args[0] && contextDescription.includes('local changes without push')) {
247
- // Case of local changes without push
248
- console.log(`💡 ${colors.yellow}To create new branch with these changes:${colors.reset}`);
249
- console.log(` git checkout -b ${result.suggestedBranchName}`);
250
- console.log(` git push -u origin ${result.suggestedBranchName}`);
251
- } else if (currentBranch !== result.suggestedBranchName) {
252
- // Normal case of comparison between branches
131
+ if (result.context.currentBranch !== result.suggestedBranchName) {
253
132
  console.log(`💡 ${colors.yellow}For renaming your current branch:${colors.reset}`);
254
133
  console.log(` git branch -m ${result.suggestedBranchName}`);
255
134
  }
@@ -257,6 +136,6 @@ export async function runAnalyzeDiff(args) {
257
136
  console.log(`💡 ${colors.yellow}Tip:${colors.reset} Use this information to create your PR on GitHub.`);
258
137
 
259
138
  } catch (e) {
260
- error('Error executing Claude: ' + e.message);
139
+ error(`Error analyzing diff: ${e.message}`);
261
140
  }
262
141
  }
@@ -8,8 +8,9 @@
8
8
  * 3. Calculate new version with optional suffix
9
9
  * 4. Update version file(s)
10
10
  * 5. [Optional] Generate and update CHANGELOG
11
- * 6. Create annotated Git tag
12
- * 7. Push tag to remote
11
+ * 6. Stage and commit changes (skipped with --no-commit)
12
+ * 7. Create annotated Git tag (skipped with --no-tag or --no-commit)
13
+ * 8. Push tag to remote (skipped with --no-push)
13
14
  */
14
15
 
15
16
  import { execSync } from 'child_process';
@@ -37,7 +38,9 @@ import {
37
38
  getRepoRoot,
38
39
  getCurrentBranch,
39
40
  verifyRemoteExists,
40
- getRemoteName
41
+ getRemoteName,
42
+ stageFiles,
43
+ createCommit
41
44
  } from '../utils/git-operations.js';
42
45
  import { getConfig } from '../config.js';
43
46
  import { showInfo, showSuccess, showError, showWarning, promptConfirmation } from '../utils/interactive-ui.js';
@@ -125,7 +128,8 @@ function parseArguments(args) {
125
128
  baseBranch: 'main',
126
129
  dryRun: false,
127
130
  noTag: false,
128
- noPush: false
131
+ noPush: false,
132
+ noCommit: false
129
133
  };
130
134
 
131
135
  // First argument should be bump type
@@ -158,6 +162,8 @@ function parseArguments(args) {
158
162
  parsed.noTag = true;
159
163
  } else if (arg === '--no-push') {
160
164
  parsed.noPush = true;
165
+ } else if (arg === '--no-commit') {
166
+ parsed.noCommit = true;
161
167
  }
162
168
  }
163
169
 
@@ -218,6 +224,7 @@ export async function runBumpVersion(args) {
218
224
  console.log(' --dry-run Preview changes without applying');
219
225
  console.log(' --no-tag Skip Git tag creation');
220
226
  console.log(' --no-push Create tag but don\'t push to remote');
227
+ console.log(' --no-commit Skip automatic commit (manual workflow)');
221
228
  console.log('');
222
229
  console.log('Examples:');
223
230
  console.log(' claude-hooks bump-version minor --suffix SNAPSHOT');
@@ -381,58 +388,136 @@ export async function runBumpVersion(args) {
381
388
  console.log('');
382
389
  }
383
390
 
384
- // Step 6: Create Git tag (if not disabled)
385
- if (!options.noTag) {
386
- logger.debug('bump-version', 'Step 6: Creating Git tag');
387
- showInfo('Creating Git tag...');
388
-
389
- // Check if tag already exists
390
- const exists = await tagExists(tagName, 'local');
391
- if (exists) {
392
- showWarning(`Tag ${tagName} already exists locally`);
393
- const shouldOverwrite = await promptConfirmation(
394
- 'Overwrite existing tag?',
395
- false
396
- );
397
-
398
- if (!shouldOverwrite) {
399
- showInfo('Tag creation skipped');
400
- console.log('');
401
- console.log('Version files updated successfully, but tag was not created.');
402
- return;
391
+ // Step 6: Stage and commit changes
392
+ let commitCreated = false;
393
+
394
+ if (!options.noCommit) {
395
+ logger.debug('bump-version', 'Step 6: Staging and committing changes');
396
+ showInfo('Staging and committing changes...');
397
+
398
+ // Collect files to stage
399
+ const filesToStage = [];
400
+
401
+ // Add discovered version files
402
+ const paths = getDiscoveredPaths();
403
+ if (paths.packageJson) {
404
+ filesToStage.push(paths.packageJson);
405
+ }
406
+ if (paths.pomXml) {
407
+ filesToStage.push(paths.pomXml);
408
+ }
409
+
410
+ // Add CHANGELOG if it was updated
411
+ if (options.updateChangelog) {
412
+ const changelogPath = path.join(getRepoRoot(), 'CHANGELOG.md');
413
+ if (fs.existsSync(changelogPath)) {
414
+ filesToStage.push(changelogPath);
415
+ }
416
+ }
417
+
418
+ // Stage files
419
+ const stageResult = stageFiles(filesToStage);
420
+
421
+ if (!stageResult.success) {
422
+ showError(`Failed to stage files: ${stageResult.error}`);
423
+ console.log('');
424
+ console.log('Files that should be staged:');
425
+ filesToStage.forEach(f => console.log(` - ${path.relative(getRepoRoot(), f)}`));
426
+ console.log('');
427
+ console.log('You can stage and commit manually:');
428
+ console.log(` git add ${filesToUpdate.join(' ')}`);
429
+ if (options.updateChangelog) {
430
+ console.log(' git add CHANGELOG.md');
403
431
  }
432
+ console.log(` git commit -m "chore(version): bump to ${newVersion}"`);
433
+ process.exit(1);
404
434
  }
405
435
 
406
- const tagMessage = `Release version ${newVersion}`;
407
- const tagResult = createTag(newVersion, tagMessage, { force: exists });
436
+ showSuccess(`✓ Staged ${stageResult.stagedFiles.length} file(s)`);
408
437
 
409
- if (tagResult.success) {
410
- showSuccess(`✓ Tag created: ${tagName}`);
438
+ // Create commit
439
+ const commitMessage = `chore(version): bump to ${newVersion}`;
440
+ const commitResult = createCommit(commitMessage, { noVerify: true });
441
+
442
+ if (!commitResult.success) {
443
+ showError(`Failed to create commit: ${commitResult.error}`);
411
444
  console.log('');
445
+ console.log('Files have been staged. You can commit manually:');
446
+ console.log(` git commit -m "chore(version): bump to ${newVersion}"`);
447
+ process.exit(1);
448
+ }
412
449
 
413
- // Step 7: Push tag (if not disabled)
414
- if (!options.noPush) {
415
- logger.debug('bump-version', 'Step 7: Pushing tag to remote');
416
- showInfo('Pushing tag to remote...');
450
+ showSuccess('✓ Changes committed');
451
+ commitCreated = true;
452
+ console.log('');
453
+ } else {
454
+ showInfo('Commit skipped (--no-commit)');
455
+ console.log('');
456
+ }
417
457
 
418
- const pushResult = pushTags(null, tagName);
458
+ // Step 7: Create Git tag (if not disabled)
459
+ if (!options.noTag) {
460
+ logger.debug('bump-version', 'Step 7: Creating Git tag');
419
461
 
420
- if (pushResult.success) {
421
- showSuccess('✓ Tag pushed to remote');
462
+ // Prevent tag creation if changes not committed
463
+ if (options.noCommit) {
464
+ showWarning('Tag creation skipped: --no-commit requires manual commit before tagging');
465
+ console.log('');
466
+ console.log('After committing, you can create the tag:');
467
+ console.log(` git tag -a ${tagName} -m "Release version ${newVersion}"`);
468
+ console.log(` git push origin ${tagName}`);
469
+ console.log('');
470
+ } else {
471
+ showInfo('Creating Git tag...');
472
+
473
+ // Check if tag already exists
474
+ const exists = await tagExists(tagName, 'local');
475
+ if (exists) {
476
+ showWarning(`Tag ${tagName} already exists locally`);
477
+ const shouldOverwrite = await promptConfirmation(
478
+ 'Overwrite existing tag?',
479
+ false
480
+ );
481
+
482
+ if (!shouldOverwrite) {
483
+ showInfo('Tag creation skipped');
484
+ console.log('');
485
+ console.log('Version files updated successfully, but tag was not created.');
486
+ return;
487
+ }
488
+ }
489
+
490
+ const tagMessage = `Release version ${newVersion}`;
491
+ const tagResult = createTag(newVersion, tagMessage, { force: exists });
492
+
493
+ if (tagResult.success) {
494
+ showSuccess(`✓ Tag created: ${tagName}`);
495
+ console.log('');
496
+
497
+ // Step 7: Push tag (if not disabled)
498
+ if (!options.noPush) {
499
+ logger.debug('bump-version', 'Step 7: Pushing tag to remote');
500
+ showInfo('Pushing tag to remote...');
501
+
502
+ const pushResult = pushTags(null, tagName);
503
+
504
+ if (pushResult.success) {
505
+ showSuccess('✓ Tag pushed to remote');
506
+ } else {
507
+ showError(`Failed to push tag: ${pushResult.error}`);
508
+ console.log('');
509
+ console.log('You can push the tag manually:');
510
+ console.log(` git push origin ${tagName}`);
511
+ }
422
512
  } else {
423
- showError(`Failed to push tag: ${pushResult.error}`);
513
+ showInfo('Tag push skipped (--no-push)');
424
514
  console.log('');
425
- console.log('You can push the tag manually:');
515
+ console.log('To push the tag later:');
426
516
  console.log(` git push origin ${tagName}`);
427
517
  }
428
518
  } else {
429
- showInfo('Tag push skipped (--no-push)');
430
- console.log('');
431
- console.log('To push the tag later:');
432
- console.log(` git push origin ${tagName}`);
519
+ showError(`Failed to create tag: ${tagResult.error}`);
433
520
  }
434
- } else {
435
- showError(`Failed to create tag: ${tagResult.error}`);
436
521
  }
437
522
  } else {
438
523
  showInfo('Tag creation skipped (--no-tag)');
@@ -448,11 +533,33 @@ export async function runBumpVersion(args) {
448
533
  console.log(`${colors.blue}New version:${colors.reset} ${newVersion}`);
449
534
  console.log(`${colors.blue}Tag:${colors.reset} ${tagName}`);
450
535
  console.log('');
451
- console.log('Next steps:');
452
- console.log(' 1. Review the changes: git diff');
453
- console.log(` 2. Commit the version bump: git add . && git commit -m "chore: bump version to ${ newVersion }"`);
454
- console.log(' 3. Create PR: claude-hooks create-pr main');
455
- console.log('');
536
+
537
+ // Conditional next steps
538
+ if (options.noCommit) {
539
+ console.log('Next steps:');
540
+ console.log(' 1. Review changes: git diff');
541
+ console.log(` 2. Stage files: git add ${filesToUpdate.join(' ')}`);
542
+ if (options.updateChangelog) {
543
+ console.log(' git add CHANGELOG.md');
544
+ }
545
+ console.log(` 3. Commit: git commit -m "chore(version): bump to ${newVersion}"`);
546
+ console.log(` 4. Create tag: git tag -a ${tagName} -m "Release version ${newVersion}"`);
547
+ console.log(` 5. Push: git push origin $(git branch --show-current) ${tagName}`);
548
+ console.log('');
549
+ } else if (commitCreated && !options.noTag && !options.noPush) {
550
+ console.log('All done! Changes committed and tag pushed to remote.');
551
+ console.log('');
552
+ console.log('Next steps:');
553
+ console.log(' 1. Create PR: claude-hooks create-pr main');
554
+ console.log('');
555
+ } else {
556
+ console.log('Next steps:');
557
+ if (commitCreated && options.noPush) {
558
+ console.log(` 1. Push changes: git push origin $(git branch --show-current) ${tagName}`);
559
+ }
560
+ console.log(' 2. Create PR: claude-hooks create-pr main');
561
+ console.log('');
562
+ }
456
563
 
457
564
  } catch (err) {
458
565
  logger.error('bump-version', 'Version bump failed', err);