claude-git-hooks 2.12.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,59 @@ 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
+
46
+ ## [2.13.0] - 2026-02-05
47
+
48
+ ### ✨ Added
49
+ - Interactive analysis command `claude-hooks analyze` - review all issues (INFO to BLOCKER) interactively before committing with auto-commit option
50
+ - Shared analysis engine module (`analysis-engine.js`) - centralized file data building, analysis orchestration, and results display for both pre-commit hooks and interactive analysis
51
+ - Support for version files in subdirectories - automatically searches parent directories when version files not found in repo root
52
+ - Auto-commit functionality after interactive analysis - creates commit with auto-generated message when user confirms
53
+
54
+ ### 🔧 Changed
55
+ - Improved version detection logic for pom.xml files - enhanced reliability of version extraction in Maven projects
56
+
57
+ ### 🐛 Fixed
58
+ - Fixed version search logic for pom.xml files - corrected parsing issues in Maven version detection
59
+
60
+
8
61
  ## [2.12.0] - 2026-02-03
9
62
 
10
63
  ### ✨ Added
@@ -43,7 +96,7 @@ y este proyecto adhiere a [Semantic Versioning](https://semver.org/spec/v2.0.0.h
43
96
 
44
97
  - **New utility modules**:
45
98
  - `lib/utils/version-manager.js` - Version detection, parsing, incrementing, validation
46
- - `lib/utils/git-tag-manager.js` - Git tag operations (create, list, compare, push)
99
+ - `lib/utils/git-tag-manager.js` - Git tag operations (create, list, compare, push, isSemverTag)
47
100
  - `lib/utils/changelog-generator.js` - CHANGELOG generation with Claude
48
101
  - `templates/GENERATE_CHANGELOG.md` - Claude prompt for changelog analysis
49
102
 
package/README.md CHANGED
@@ -60,6 +60,33 @@ export GITHUB_TOKEN="ghp_..."
60
60
 
61
61
  Create token at https://github.com/settings/tokens with scopes: `repo`, `read:org`
62
62
 
63
+ ### Analyze Code (Interactive Review)
64
+
65
+ Run interactive code analysis before committing:
66
+
67
+ ```bash
68
+ # Analyze staged changes (default)
69
+ claude-hooks analyze
70
+
71
+ # Analyze unstaged changes
72
+ claude-hooks analyze --unstaged
73
+
74
+ # Analyze all tracked files
75
+ claude-hooks analyze --all
76
+ ```
77
+
78
+ **What it does:**
79
+ - Analyzes selected file scope (staged, unstaged, or all)
80
+ - Shows all issues (INFO, MINOR, MAJOR, CRITICAL, BLOCKER)
81
+ - Interactive prompt with options:
82
+ - **Continue**: Creates commit automatically with auto-generated message
83
+ - **Abort**: Generate resolution prompt and fix issues
84
+ - **View**: Show detailed issue list
85
+ - Executes `git commit -m "auto" --no-verify` on confirmation
86
+ - Works outside git hooks (no stdin limitations)
87
+
88
+ **Use case:** Complete analysis-to-commit workflow in one command.
89
+
63
90
  ### Analyze Diff (without creating PR)
64
91
 
65
92
  ```bash
@@ -83,12 +110,16 @@ claude-hooks bump-version major --update-changelog
83
110
 
84
111
  # Preview without applying
85
112
  claude-hooks bump-version patch --dry-run
113
+
114
+ # Manual workflow (skip automatic commit)
115
+ claude-hooks bump-version patch --no-commit
86
116
  ```
87
117
 
88
118
  **What it does:**
89
119
  - Detects project type (Node.js, Maven, or monorepo with both)
90
120
  - Updates `package.json` and/or `pom.xml`
91
121
  - Generates CHANGELOG entry with Claude (analyzes commits)
122
+ - Commits changes automatically with conventional commit format
92
123
  - Creates annotated Git tag with `v` prefix (e.g., `v2.7.0`)
93
124
  - Pushes tag to remote automatically
94
125
 
@@ -223,9 +254,11 @@ claude-hooks --help # Full command reference
223
254
 
224
255
  | Module | Purpose | Key Exports |
225
256
  |--------|---------|-------------|
257
+ | `analysis-engine.js` | **Shared analysis logic** - file data, orchestration, results (v2.13.0) | `buildFilesData()`, `runAnalysis()`, `consolidateResults()`, `displayResults()` |
226
258
  | `claude-client.js` | **Claude CLI wrapper** - spawn, retry, parallel execution | `analyzeCode()`, `analyzeCodeParallel()`, `executeClaudeWithRetry()` |
227
259
  | `prompt-builder.js` | **Prompt construction** - load templates, replace variables | `buildAnalysisPrompt()`, `loadPrompt()` |
228
- | `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()` |
229
262
  | `github-api.js` | **Octokit integration** - PR creation, token validation | `createPullRequest()`, `validateToken()`, `saveGitHubToken()` |
230
263
  | `github-client.js` | **GitHub helpers** - CODEOWNERS parsing, reviewers | `getReviewersForFiles()`, `parseGitHubRepo()` |
231
264
  | `preset-loader.js` | **Preset system** - load tech-stack configurations | `loadPreset()`, `listPresets()` |
@@ -242,12 +275,16 @@ claude-hooks --help # Full command reference
242
275
  ```
243
276
  git commit → templates/pre-commit (bash wrapper)
244
277
  → lib/hooks/pre-commit.js
245
- → getStagedFiles() → filter by preset extensions
246
- buildAnalysisPrompt() → analyzeCode() or analyzeCodeParallel()
247
- parse JSON response exit 0 (pass) or exit 1 (block)
248
- → if blocked: generates claude_resolution_prompt.md
278
+ → getStagedFiles() → filterFiles() by preset extensions + size
279
+ buildFilesData() → runAnalysis() (via analysis-engine.js)
280
+ displayResults()show quality gate status
281
+ → if blocking issues (critical/blocker):
282
+ generates claude_resolution_prompt.md → exit 1 (block)
283
+ → if non-blocking or no issues: exit 0 (pass)
249
284
  ```
250
285
 
286
+ **Note:** For interactive review of non-blocking issues, use `claude-hooks analyze` before committing.
287
+
251
288
  #### Commit Message Generation
252
289
 
253
290
  ```
@@ -258,16 +295,18 @@ git commit -m "auto" → templates/prepare-commit-msg (bash wrapper)
258
295
  → write to COMMIT_EDITMSG
259
296
  ```
260
297
 
261
- #### PR Creation
298
+ #### PR Metadata Generation (analyze-diff / create-pr)
262
299
 
263
300
  ```
264
- claude-hooks create-pr → bin/claude-hooks (router)
265
- → lib/commands/create-pr.js
266
- validateToken() (github-api.js)
267
- parseGitHubRepo() (github-client.js)
268
- getReviewersForFiles() (CODEOWNERS + config)
269
- executeClaudeWithRetry() (metadata generation)
270
- 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
271
310
  ```
272
311
 
273
312
  ### Config Priority
package/bin/claude-hooks CHANGED
@@ -13,6 +13,7 @@ import { error } from '../lib/commands/helpers.js';
13
13
  // Import commands
14
14
  import { runInstall } from '../lib/commands/install.js';
15
15
  import { runEnable, runDisable, runStatus, runUninstall } from '../lib/commands/hooks.js';
16
+ import { runAnalyze } from '../lib/commands/analyze.js';
16
17
  import { runAnalyzeDiff } from '../lib/commands/analyze-diff.js';
17
18
  import { runCreatePr } from '../lib/commands/create-pr.js';
18
19
  import { runSetupGitHub } from '../lib/commands/setup-github.js';
@@ -51,6 +52,13 @@ async function main() {
51
52
  case 'status':
52
53
  runStatus();
53
54
  break;
55
+ case 'analyze':
56
+ await runAnalyze({
57
+ staged: !args.includes('--unstaged') && !args.includes('--all'),
58
+ unstaged: args.includes('--unstaged'),
59
+ all: args.includes('--all')
60
+ });
61
+ break;
54
62
  case 'analyze-diff':
55
63
  await runAnalyzeDiff(args.slice(1));
56
64
  break;
@@ -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
  }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * File: analyze.js
3
+ * Purpose: On-demand code analysis command (interactive, runs outside git hooks)
4
+ *
5
+ * Why this exists: Git hooks cannot reliably read stdin for interactive prompts
6
+ * (stdin is redirected from /dev/null). This command provides interactive analysis
7
+ * before committing, allowing developers to review all issues and decide whether
8
+ * to proceed or fix them first.
9
+ *
10
+ * Key features:
11
+ * - Runs outside git hook context (stdin works normally)
12
+ * - Analyzes staged, unstaged, or all tracked files
13
+ * - Interactive confirmation with detailed issue view
14
+ * - Generates resolution prompt on abort
15
+ *
16
+ * Usage:
17
+ * claude-hooks analyze # Analyze staged files (default)
18
+ * claude-hooks analyze --unstaged # Analyze unstaged changes
19
+ * claude-hooks analyze --all # Analyze all tracked files
20
+ */
21
+
22
+ import { getStagedFiles, getUnstagedFiles, getAllTrackedFiles, createCommit } from '../utils/git-operations.js';
23
+ import { filterFiles } from '../utils/file-operations.js';
24
+ import {
25
+ buildFilesData,
26
+ runAnalysis,
27
+ hasAnyIssues,
28
+ displayIssueSummary
29
+ } from '../utils/analysis-engine.js';
30
+ import { promptUserConfirmation, promptConfirmation } from '../utils/interactive-ui.js';
31
+ import { generateResolutionPrompt } from '../utils/resolution-prompt.js';
32
+ import { getConfig } from '../config.js';
33
+ import { loadPreset } from '../utils/preset-loader.js';
34
+ import logger from '../utils/logger.js';
35
+ import { error, success, info } from './helpers.js';
36
+
37
+ /**
38
+ * Main analyze command
39
+ * Why: Provides interactive analysis before committing
40
+ *
41
+ * @param {Object} options - Command options
42
+ * @param {boolean} options.staged - Analyze staged files (default: true)
43
+ * @param {boolean} options.unstaged - Analyze unstaged files
44
+ * @param {boolean} options.all - Analyze all tracked files
45
+ */
46
+ export const runAnalyze = async (options = {}) => {
47
+ const { unstaged = false, all = false } = options;
48
+
49
+ try {
50
+ // Load configuration
51
+ const config = await getConfig();
52
+
53
+ // Enable debug mode from config
54
+ if (config.system?.debug) {
55
+ logger.setDebugMode(true);
56
+ }
57
+
58
+ // Load active preset for file extensions
59
+ const presetName = config.preset || 'default';
60
+ const { metadata } = await loadPreset(presetName);
61
+ const allowedExtensions = metadata.fileExtensions;
62
+
63
+ // Determine scope
64
+ let scopeLabel = 'staged changes';
65
+ if (all) {
66
+ scopeLabel = 'all tracked files';
67
+ } else if (unstaged) {
68
+ scopeLabel = 'unstaged changes';
69
+ }
70
+
71
+ info(`Analyzing ${scopeLabel} with '${metadata.displayName}' preset...`);
72
+
73
+ // Get files based on scope
74
+ let files = [];
75
+ if (all) {
76
+ files = getAllTrackedFiles({ extensions: allowedExtensions });
77
+ } else if (unstaged) {
78
+ files = getUnstagedFiles({ extensions: allowedExtensions });
79
+ } else {
80
+ files = getStagedFiles({ extensions: allowedExtensions });
81
+ }
82
+
83
+ if (files.length === 0) {
84
+ info(`No files to analyze in ${scopeLabel}.`);
85
+ process.exit(0);
86
+ }
87
+
88
+ logger.debug('analyze', 'Files found', {
89
+ scope: scopeLabel,
90
+ count: files.length,
91
+ extensions: allowedExtensions
92
+ });
93
+
94
+ // Filter files by size
95
+ const filteredFiles = await filterFiles(files, {
96
+ maxSize: config.analysis?.maxFileSize || 1048576,
97
+ extensions: allowedExtensions
98
+ });
99
+
100
+ const validFiles = filteredFiles.filter(f => f.valid);
101
+ const invalidFiles = filteredFiles.filter(f => !f.valid);
102
+
103
+ // Show warnings for skipped files
104
+ if (invalidFiles.length > 0) {
105
+ invalidFiles.forEach(file => {
106
+ logger.warning(`Skipping ${file.path}: ${file.reason}`);
107
+ });
108
+ }
109
+
110
+ if (validFiles.length === 0) {
111
+ info(`No valid files found to analyze in ${scopeLabel}.`);
112
+ process.exit(0);
113
+ }
114
+
115
+ info(`Sending ${validFiles.length} file(s) for analysis...`);
116
+
117
+ // Build file data (diff/content) using shared engine
118
+ const filesData = buildFilesData(validFiles, { staged: !unstaged && !all });
119
+
120
+ if (filesData.length === 0) {
121
+ info('No file data could be extracted.');
122
+ process.exit(0);
123
+ }
124
+
125
+ // Run analysis using shared engine
126
+ const result = await runAnalysis(filesData, config, { hook: 'analyze' });
127
+
128
+ // Check results
129
+ if (!hasAnyIssues(result)) {
130
+ console.log('');
131
+ success('No issues found. Code is ready to commit!');
132
+ console.log('');
133
+
134
+ // Prompt user to commit or cancel
135
+ const shouldCommit = await promptConfirmation('Create commit now?', true);
136
+
137
+ if (shouldCommit) {
138
+ info('Creating commit with auto-generated message...');
139
+ console.log('');
140
+
141
+ const commitResult = createCommit('auto', { noVerify: true });
142
+
143
+ if (commitResult.success) {
144
+ success('Commit created successfully!');
145
+ if (commitResult.output) {
146
+ console.log(commitResult.output);
147
+ }
148
+ console.log('');
149
+ process.exit(0);
150
+ } else {
151
+ error(`Commit failed: ${commitResult.error}`);
152
+ console.log('');
153
+ process.exit(1);
154
+ }
155
+ } else {
156
+ info('Commit cancelled. Staged files remain unchanged.');
157
+ console.log('');
158
+ process.exit(0);
159
+ }
160
+ }
161
+
162
+ // Display summary
163
+ console.log('');
164
+ console.log('Analysis complete:');
165
+ displayIssueSummary(result);
166
+ console.log('');
167
+
168
+ // Interactive confirmation (works outside git hook)
169
+ const userChoice = await promptUserConfirmation(result);
170
+
171
+ if (userChoice === 'abort') {
172
+ // Generate resolution prompt
173
+ await generateResolutionPrompt(result, {
174
+ fileCount: filesData.length
175
+ });
176
+ console.log('');
177
+ success('Resolution prompt generated: claude_resolution_prompt.md');
178
+ info('Fix issues and run `claude-hooks analyze` again.');
179
+ console.log('');
180
+ process.exit(1);
181
+ } else {
182
+ // User chose to continue - execute commit automatically
183
+ console.log('');
184
+
185
+ // Safeguard: verify staged files still exist
186
+ const currentStagedFiles = getStagedFiles({ extensions: allowedExtensions });
187
+ if (currentStagedFiles.length === 0) {
188
+ error('No staged files found. Did you unstage changes?');
189
+ process.exit(1);
190
+ }
191
+
192
+ info('Creating commit with auto-generated message...');
193
+ console.log('');
194
+
195
+ // Execute commit with --no-verify (skip hooks - we already analyzed)
196
+ const commitResult = createCommit('auto', { noVerify: true });
197
+
198
+ if (commitResult.success) {
199
+ success('Commit created successfully!');
200
+ if (commitResult.output) {
201
+ console.log(commitResult.output);
202
+ }
203
+ console.log('');
204
+ process.exit(0);
205
+ } else {
206
+ error(`Commit failed: ${commitResult.error}`);
207
+ console.log('');
208
+ process.exit(1);
209
+ }
210
+ }
211
+
212
+ } catch (err) {
213
+ logger.error('analyze', 'Analysis failed', err);
214
+ error(`Analysis failed: ${err.message}`);
215
+ process.exit(1);
216
+ }
217
+ };