claude-git-hooks 2.9.1 → 2.10.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.
- package/CHANGELOG.md +107 -0
- package/README.md +209 -755
- package/bin/claude-hooks +97 -2310
- package/lib/commands/analyze-diff.js +262 -0
- package/lib/commands/create-pr.js +374 -0
- package/lib/commands/debug.js +52 -0
- package/lib/commands/help.js +147 -0
- package/lib/commands/helpers.js +389 -0
- package/lib/commands/hooks.js +150 -0
- package/lib/commands/install.js +688 -0
- package/lib/commands/migrate-config.js +103 -0
- package/lib/commands/presets.js +101 -0
- package/lib/commands/setup-github.js +93 -0
- package/lib/commands/telemetry-cmd.js +48 -0
- package/lib/commands/update.js +67 -0
- package/lib/utils/github-api.js +87 -17
- package/lib/utils/github-client.js +9 -550
- package/package.json +1 -1
- package/lib/utils/mcp-setup.js +0 -342
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: analyze-diff.js
|
|
3
|
+
* Purpose: Analyze differences between branches and generate PR info
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import { executeClaudeWithRetry, extractJSON } from '../utils/claude-client.js';
|
|
9
|
+
import { loadPrompt } from '../utils/prompt-builder.js';
|
|
10
|
+
import { getConfig } from '../config.js';
|
|
11
|
+
import {
|
|
12
|
+
colors,
|
|
13
|
+
error,
|
|
14
|
+
success,
|
|
15
|
+
info,
|
|
16
|
+
warning,
|
|
17
|
+
checkGitRepo
|
|
18
|
+
} from './helpers.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Analyze-diff command
|
|
22
|
+
* @param {Array<string>} args - Command arguments
|
|
23
|
+
*/
|
|
24
|
+
export async function runAnalyzeDiff(args) {
|
|
25
|
+
if (!checkGitRepo()) {
|
|
26
|
+
error('You are not in a Git repository.');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Load configuration
|
|
31
|
+
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;
|
|
38
|
+
}
|
|
39
|
+
|
|
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)`;
|
|
82
|
+
|
|
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}...`);
|
|
95
|
+
|
|
96
|
+
// Get modified files
|
|
97
|
+
let diffFiles;
|
|
98
|
+
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();
|
|
105
|
+
|
|
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
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
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
|
+
|
|
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);
|
|
178
|
+
|
|
179
|
+
// Show the results
|
|
180
|
+
console.log('');
|
|
181
|
+
console.log('════════════════════════════════════════════════════════════════');
|
|
182
|
+
console.log(' DIFFERENCES ANALYSIS ');
|
|
183
|
+
console.log('════════════════════════════════════════════════════════════════');
|
|
184
|
+
console.log('');
|
|
185
|
+
|
|
186
|
+
console.log(`🔍 ${colors.blue}Context:${colors.reset} ${contextDescription}`);
|
|
187
|
+
console.log(`📊 ${colors.blue}Changed Files:${colors.reset} ${filesChanged}`);
|
|
188
|
+
console.log('');
|
|
189
|
+
|
|
190
|
+
console.log(`📝 ${colors.green}Pull Request Title:${colors.reset}`);
|
|
191
|
+
console.log(` ${result.prTitle}`);
|
|
192
|
+
console.log('');
|
|
193
|
+
|
|
194
|
+
console.log(`🌿 ${colors.green}Suggested branch name:${colors.reset}`);
|
|
195
|
+
console.log(` ${result.suggestedBranchName}`);
|
|
196
|
+
console.log('');
|
|
197
|
+
|
|
198
|
+
console.log(`📋 ${colors.green}Type of change:${colors.reset} ${result.changeType}`);
|
|
199
|
+
|
|
200
|
+
if (result.breakingChanges) {
|
|
201
|
+
console.log(`⚠️ ${colors.yellow}Breaking Changes: SÍ${colors.reset}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
console.log('');
|
|
205
|
+
console.log(`📄 ${colors.green}Pull Request Description:${colors.reset}`);
|
|
206
|
+
console.log('───────────────────────────────────────────────────────────────');
|
|
207
|
+
console.log(result.prDescription);
|
|
208
|
+
console.log('───────────────────────────────────────────────────────────────');
|
|
209
|
+
|
|
210
|
+
if (result.testingNotes) {
|
|
211
|
+
console.log('');
|
|
212
|
+
console.log(`🧪 ${colors.green}Testing notes:${colors.reset}`);
|
|
213
|
+
console.log(result.testingNotes);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Save the results in a file with context
|
|
217
|
+
const outputData = {
|
|
218
|
+
...result,
|
|
219
|
+
context: {
|
|
220
|
+
currentBranch,
|
|
221
|
+
baseBranch,
|
|
222
|
+
contextDescription,
|
|
223
|
+
filesChanged,
|
|
224
|
+
timestamp: new Date().toISOString()
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// Ensure .claude/out directory exists
|
|
229
|
+
const outputDir = '.claude/out';
|
|
230
|
+
if (!fs.existsSync(outputDir)) {
|
|
231
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const outputFile = '.claude/out/pr-analysis.json';
|
|
235
|
+
fs.writeFileSync(outputFile, JSON.stringify(outputData, null, 2));
|
|
236
|
+
|
|
237
|
+
const elapsed = Date.now() - startTime;
|
|
238
|
+
const seconds = Math.floor(elapsed / 1000);
|
|
239
|
+
const ms = elapsed % 1000;
|
|
240
|
+
console.log('');
|
|
241
|
+
console.log(`${colors.blue}⏱️ Analysis completed in ${seconds}.${ms}s${colors.reset}`);
|
|
242
|
+
info(`Results saved in ${outputFile}`);
|
|
243
|
+
|
|
244
|
+
// Contextual suggestions
|
|
245
|
+
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
|
|
253
|
+
console.log(`💡 ${colors.yellow}For renaming your current branch:${colors.reset}`);
|
|
254
|
+
console.log(` git branch -m ${result.suggestedBranchName}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
console.log(`💡 ${colors.yellow}Tip:${colors.reset} Use this information to create your PR on GitHub.`);
|
|
258
|
+
|
|
259
|
+
} catch (e) {
|
|
260
|
+
error('Error executing Claude: ' + e.message);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: create-pr.js
|
|
3
|
+
* Purpose: Create pull request with auto-generated metadata and reviewers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { executeClaudeWithRetry, extractJSON } from '../utils/claude-client.js';
|
|
10
|
+
import { loadPrompt } from '../utils/prompt-builder.js';
|
|
11
|
+
import { getConfig } from '../config.js';
|
|
12
|
+
import { getOrPromptTaskId, formatWithTaskId } from '../utils/task-id.js';
|
|
13
|
+
import { getReviewersForFiles } from '../utils/github-client.js';
|
|
14
|
+
import { showPRPreview, promptMenu, showSuccess, showError, showInfo, showWarning } from '../utils/interactive-ui.js';
|
|
15
|
+
import logger from '../utils/logger.js';
|
|
16
|
+
import {
|
|
17
|
+
error,
|
|
18
|
+
checkGitRepo
|
|
19
|
+
} from './helpers.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create PR command (v2.5.0+ - Octokit-based)
|
|
23
|
+
* @param {Array<string>} args - Command arguments
|
|
24
|
+
*/
|
|
25
|
+
export async function runCreatePr(args) {
|
|
26
|
+
logger.debug('create-pr', 'Starting create-pr command', { args });
|
|
27
|
+
|
|
28
|
+
if (!checkGitRepo()) {
|
|
29
|
+
error('You are not in a Git repository.');
|
|
30
|
+
logger.debug('create-pr', 'Not in a git repository, exiting');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
// Load configuration
|
|
36
|
+
logger.debug('create-pr', 'Loading configuration');
|
|
37
|
+
const config = await getConfig();
|
|
38
|
+
logger.debug('create-pr', 'Configuration loaded', {
|
|
39
|
+
preset: config.preset,
|
|
40
|
+
githubEnabled: config.github?.enabled,
|
|
41
|
+
defaultBase: config.github?.pr?.defaultBase
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Import GitHub API module
|
|
45
|
+
logger.debug('create-pr', 'Importing GitHub API modules');
|
|
46
|
+
const { createPullRequest, GitHubAPIError, validateToken, findExistingPR } = await import('../utils/github-api.js');
|
|
47
|
+
const { parseGitHubRepo } = await import('../utils/github-client.js');
|
|
48
|
+
|
|
49
|
+
showInfo('🚀 Creating Pull Request...');
|
|
50
|
+
console.log('');
|
|
51
|
+
|
|
52
|
+
// Step 1: Validate GitHub token
|
|
53
|
+
logger.debug('create-pr', 'Step 1: Validating GitHub token');
|
|
54
|
+
const tokenValidation = await validateToken();
|
|
55
|
+
if (!tokenValidation.valid) {
|
|
56
|
+
logger.error('create-pr', 'GitHub authentication failed', { error: tokenValidation.error });
|
|
57
|
+
showError('GitHub authentication failed');
|
|
58
|
+
console.log('');
|
|
59
|
+
console.log('Please configure your GitHub token:');
|
|
60
|
+
console.log(' Option 1: Set GITHUB_TOKEN environment variable');
|
|
61
|
+
console.log(' Option 2: Add token to .claude/settings.local.json:');
|
|
62
|
+
console.log(' { "githubToken": "ghp_your_token_here" }');
|
|
63
|
+
console.log(' Option 3: Run: claude-hooks setup-github');
|
|
64
|
+
console.log('');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
logger.debug('create-pr', 'Token validation successful', {
|
|
69
|
+
user: tokenValidation.user,
|
|
70
|
+
hasRepoScope: tokenValidation.hasRepoScope,
|
|
71
|
+
scopes: tokenValidation.scopes
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
showSuccess(`Authenticated as: ${tokenValidation.user}`);
|
|
75
|
+
if (!tokenValidation.hasRepoScope) {
|
|
76
|
+
showWarning('Token may lack "repo" scope - PR creation might fail');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Step 2: Get or prompt for task-id (with config for pattern)
|
|
80
|
+
logger.debug('create-pr', 'Step 2: Getting or prompting for task-id');
|
|
81
|
+
const taskId = await getOrPromptTaskId({
|
|
82
|
+
prompt: true, // DO prompt for PRs (unlike commit messages)
|
|
83
|
+
required: false, // Allow skipping
|
|
84
|
+
config: config // Pass config for custom pattern
|
|
85
|
+
});
|
|
86
|
+
logger.debug('create-pr', 'Task ID determined', { taskId });
|
|
87
|
+
|
|
88
|
+
// Step 3: Parse arguments and determine base branch
|
|
89
|
+
logger.debug('create-pr', 'Step 3: Parsing arguments and determining base branch', { args });
|
|
90
|
+
let baseBranchArg = args[0];
|
|
91
|
+
if (baseBranchArg && /^[A-Z]{2,10}-\d+$/i.test(baseBranchArg)) {
|
|
92
|
+
baseBranchArg = args[1];
|
|
93
|
+
}
|
|
94
|
+
const baseBranch = baseBranchArg || config.github?.pr?.defaultBase || 'develop';
|
|
95
|
+
logger.debug('create-pr', 'Base branch determined', { baseBranch, fromConfig: !baseBranchArg });
|
|
96
|
+
|
|
97
|
+
// Step 4: Get current branch and repo info
|
|
98
|
+
logger.debug('create-pr', 'Step 4: Getting current branch and repo info');
|
|
99
|
+
const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
|
|
100
|
+
if (!currentBranch) {
|
|
101
|
+
logger.error('create-pr', 'Could not determine current branch');
|
|
102
|
+
error('Could not determine current branch');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const repoInfo = parseGitHubRepo();
|
|
107
|
+
logger.debug('create-pr', 'Repository and branch info', {
|
|
108
|
+
owner: repoInfo.owner,
|
|
109
|
+
repo: repoInfo.repo,
|
|
110
|
+
currentBranch,
|
|
111
|
+
baseBranch
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
showInfo(`Repository: ${repoInfo.fullName}`);
|
|
115
|
+
showInfo(`Branch: ${currentBranch} → ${baseBranch}`);
|
|
116
|
+
|
|
117
|
+
// Step 5: Check for existing PR
|
|
118
|
+
logger.debug('create-pr', 'Step 5: Checking for existing PR');
|
|
119
|
+
const existingPR = await findExistingPR({
|
|
120
|
+
owner: repoInfo.owner,
|
|
121
|
+
repo: repoInfo.repo,
|
|
122
|
+
head: currentBranch,
|
|
123
|
+
base: baseBranch
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (existingPR) {
|
|
127
|
+
logger.debug('create-pr', 'Existing PR found, exiting', {
|
|
128
|
+
prNumber: existingPR.number,
|
|
129
|
+
prUrl: existingPR.html_url
|
|
130
|
+
});
|
|
131
|
+
showWarning(`A PR already exists for this branch: #${existingPR.number}`);
|
|
132
|
+
console.log(` ${existingPR.html_url}`);
|
|
133
|
+
console.log('');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
logger.debug('create-pr', 'No existing PR found, continuing');
|
|
138
|
+
|
|
139
|
+
// Step 6: Update remote and check for differences
|
|
140
|
+
logger.debug('create-pr', 'Step 6: Fetching latest changes from remote');
|
|
141
|
+
execSync('git fetch', { stdio: 'ignore' });
|
|
142
|
+
const compareWith = `origin/${baseBranch}...HEAD`;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
execSync(`git rev-parse --verify origin/${baseBranch}`, { stdio: 'ignore' });
|
|
146
|
+
} catch (e) {
|
|
147
|
+
error(`Base branch origin/${baseBranch} does not exist`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let diffFiles;
|
|
152
|
+
try {
|
|
153
|
+
diffFiles = execSync(`git diff ${compareWith} --name-only`, { encoding: 'utf8' }).trim();
|
|
154
|
+
if (!diffFiles) {
|
|
155
|
+
showWarning('No differences with remote branch. Nothing to create a PR for.');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
} catch (e) {
|
|
159
|
+
error('Error getting differences: ' + e.message);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const filesArray = diffFiles.split('\n').filter(f => f.trim());
|
|
164
|
+
logger.debug('create-pr', 'Modified files detected', {
|
|
165
|
+
fileCount: filesArray.length,
|
|
166
|
+
files: filesArray
|
|
167
|
+
});
|
|
168
|
+
showInfo(`Found ${filesArray.length} modified file(s)`);
|
|
169
|
+
|
|
170
|
+
// Step 7: Generate PR metadata with Claude (reuse analyze-diff logic)
|
|
171
|
+
logger.debug('create-pr', 'Step 7: Generating PR metadata with Claude');
|
|
172
|
+
let fullDiff, commits;
|
|
173
|
+
try {
|
|
174
|
+
fullDiff = execSync(`git diff ${compareWith}`, { encoding: 'utf8' });
|
|
175
|
+
commits = execSync(`git log origin/${baseBranch}..HEAD --oneline`, { encoding: 'utf8' }).trim();
|
|
176
|
+
} catch (e) {
|
|
177
|
+
error('Error getting diff or commits: ' + e.message);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const truncatedDiff = fullDiff.length > 50000
|
|
182
|
+
? fullDiff.substring(0, 50000) + '\n... (truncated)'
|
|
183
|
+
: fullDiff;
|
|
184
|
+
|
|
185
|
+
const contextDescription = `${currentBranch} vs origin/${baseBranch}`;
|
|
186
|
+
const prompt = await loadPrompt('ANALYZE_DIFF.md', {
|
|
187
|
+
CONTEXT_DESCRIPTION: contextDescription,
|
|
188
|
+
SUBAGENT_INSTRUCTION: '',
|
|
189
|
+
COMMITS: commits,
|
|
190
|
+
DIFF_FILES: diffFiles,
|
|
191
|
+
FULL_DIFF: truncatedDiff
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
showInfo('Generating PR metadata with Claude...');
|
|
195
|
+
logger.debug('create-pr', 'Calling Claude with prompt', { promptLength: prompt.length });
|
|
196
|
+
|
|
197
|
+
// Prepare telemetry context for create-pr
|
|
198
|
+
const telemetryContext = {
|
|
199
|
+
fileCount: filesArray.length,
|
|
200
|
+
batchSize: filesArray.length,
|
|
201
|
+
totalBatches: 1,
|
|
202
|
+
model: 'sonnet', // create-pr always uses main model
|
|
203
|
+
hook: 'create-pr'
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const response = await executeClaudeWithRetry(prompt, {
|
|
207
|
+
timeout: 180000,
|
|
208
|
+
telemetryContext
|
|
209
|
+
});
|
|
210
|
+
logger.debug('create-pr', 'Claude response received', { responseLength: response.length });
|
|
211
|
+
|
|
212
|
+
const analysisResult = extractJSON(response);
|
|
213
|
+
logger.debug('create-pr', 'Analysis result extracted', {
|
|
214
|
+
hasResult: !!analysisResult,
|
|
215
|
+
hasPrTitle: !!analysisResult?.prTitle
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
if (!analysisResult || !analysisResult.prTitle) {
|
|
219
|
+
logger.error('create-pr', 'Failed to generate PR metadata from analysis', { analysisResult });
|
|
220
|
+
error('Failed to generate PR metadata from analysis');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Step 8: Prepare PR data
|
|
225
|
+
logger.debug('create-pr', 'Step 8: Preparing PR data');
|
|
226
|
+
let prTitle = analysisResult.prTitle;
|
|
227
|
+
if (taskId) {
|
|
228
|
+
prTitle = formatWithTaskId(prTitle, taskId);
|
|
229
|
+
logger.debug('create-pr', 'Task ID added to title', { prTitle });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const prBody = analysisResult.prDescription || analysisResult.description || '';
|
|
233
|
+
logger.debug('create-pr', 'PR title and body prepared', {
|
|
234
|
+
titleLength: prTitle.length,
|
|
235
|
+
bodyLength: prBody.length
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Step 9: Get labels from preset
|
|
239
|
+
logger.debug('create-pr', 'Step 9: Getting labels from preset');
|
|
240
|
+
let labels = [];
|
|
241
|
+
if (config.preset && config.github?.pr?.labelRules) {
|
|
242
|
+
labels = config.github.pr.labelRules[config.preset] || [];
|
|
243
|
+
}
|
|
244
|
+
if (analysisResult.breakingChanges) {
|
|
245
|
+
labels.push('breaking-change');
|
|
246
|
+
}
|
|
247
|
+
logger.debug('create-pr', 'Labels determined', { labels, preset: config.preset });
|
|
248
|
+
|
|
249
|
+
// Step 10: Get reviewers from CODEOWNERS and config
|
|
250
|
+
logger.debug('create-pr', 'Step 10: Getting reviewers from CODEOWNERS and config');
|
|
251
|
+
const reviewers = await getReviewersForFiles(filesArray, config.github?.pr);
|
|
252
|
+
logger.debug('create-pr', 'Reviewers determined', { reviewers, sources: 'CODEOWNERS + config' });
|
|
253
|
+
|
|
254
|
+
// Step 11: Show PR preview
|
|
255
|
+
const prData = {
|
|
256
|
+
title: prTitle,
|
|
257
|
+
body: prBody,
|
|
258
|
+
head: currentBranch,
|
|
259
|
+
base: baseBranch,
|
|
260
|
+
labels,
|
|
261
|
+
reviewers
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
showPRPreview(prData);
|
|
265
|
+
|
|
266
|
+
// Step 12: Prompt for confirmation
|
|
267
|
+
const action = await promptMenu(
|
|
268
|
+
'What would you like to do?',
|
|
269
|
+
[
|
|
270
|
+
{ key: 'c', label: 'Create PR' },
|
|
271
|
+
{ key: 'x', label: 'Cancel' }
|
|
272
|
+
],
|
|
273
|
+
'c'
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
if (action === 'x') {
|
|
277
|
+
showInfo('PR creation cancelled');
|
|
278
|
+
|
|
279
|
+
// Save metadata for later use
|
|
280
|
+
const outputDir = '.claude/out';
|
|
281
|
+
if (!fs.existsSync(outputDir)) {
|
|
282
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
283
|
+
}
|
|
284
|
+
const outputFile = path.join(outputDir, 'pr-metadata.json');
|
|
285
|
+
fs.writeFileSync(outputFile, JSON.stringify(prData, null, 2));
|
|
286
|
+
showInfo(`PR metadata saved to ${outputFile}`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Step 13: Create PR via Octokit
|
|
291
|
+
logger.debug('create-pr', 'Step 13: Creating PR via Octokit');
|
|
292
|
+
showInfo('Creating pull request on GitHub...');
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
logger.debug('create-pr', 'Calling createPullRequest API', {
|
|
296
|
+
owner: repoInfo.owner,
|
|
297
|
+
repo: repoInfo.repo,
|
|
298
|
+
head: prData.head,
|
|
299
|
+
base: prData.base
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const result = await createPullRequest({
|
|
303
|
+
owner: repoInfo.owner,
|
|
304
|
+
repo: repoInfo.repo,
|
|
305
|
+
title: prData.title,
|
|
306
|
+
body: prData.body,
|
|
307
|
+
head: prData.head,
|
|
308
|
+
base: prData.base,
|
|
309
|
+
draft: false,
|
|
310
|
+
labels: prData.labels,
|
|
311
|
+
reviewers: prData.reviewers
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
logger.debug('create-pr', 'PR created successfully', {
|
|
315
|
+
prNumber: result.number,
|
|
316
|
+
prUrl: result.html_url
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
console.log('');
|
|
320
|
+
showSuccess('Pull request created successfully!');
|
|
321
|
+
console.log('');
|
|
322
|
+
console.log(` PR #${result.number}: ${result.html_url}`);
|
|
323
|
+
console.log('');
|
|
324
|
+
|
|
325
|
+
if (result.reviewers.length > 0) {
|
|
326
|
+
showInfo(`Reviewers requested: ${result.reviewers.join(', ')}`);
|
|
327
|
+
}
|
|
328
|
+
if (result.labels.length > 0) {
|
|
329
|
+
showInfo(`Labels added: ${result.labels.join(', ')}`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
} catch (apiError) {
|
|
333
|
+
logger.error('create-pr', 'Failed to create pull request', apiError);
|
|
334
|
+
showError('Failed to create pull request');
|
|
335
|
+
console.error('');
|
|
336
|
+
console.error(` ${apiError.message}`);
|
|
337
|
+
|
|
338
|
+
if (apiError.context?.suggestion) {
|
|
339
|
+
console.error('');
|
|
340
|
+
console.error(` 💡 ${apiError.context.suggestion}`);
|
|
341
|
+
}
|
|
342
|
+
console.error('');
|
|
343
|
+
|
|
344
|
+
// Save PR metadata for manual creation or retry
|
|
345
|
+
const outputDir = '.claude/out';
|
|
346
|
+
if (!fs.existsSync(outputDir)) {
|
|
347
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
348
|
+
}
|
|
349
|
+
const outputFile = path.join(outputDir, 'pr-metadata.json');
|
|
350
|
+
fs.writeFileSync(outputFile, JSON.stringify({
|
|
351
|
+
...prData,
|
|
352
|
+
error: apiError.message,
|
|
353
|
+
timestamp: new Date().toISOString()
|
|
354
|
+
}, null, 2));
|
|
355
|
+
|
|
356
|
+
logger.debug('create-pr', 'PR metadata saved', { outputFile });
|
|
357
|
+
showInfo(`PR metadata saved to ${outputFile}`);
|
|
358
|
+
showInfo('You can create the PR manually using this data');
|
|
359
|
+
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
} catch (err) {
|
|
364
|
+
logger.error('create-pr', 'Error creating PR', err);
|
|
365
|
+
showError('Error creating PR: ' + err.message);
|
|
366
|
+
|
|
367
|
+
if (err.context) {
|
|
368
|
+
logger.debug('create-pr', 'Error context', err.context);
|
|
369
|
+
console.error('Context:', JSON.stringify(err.context, null, 2));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: debug.js
|
|
3
|
+
* Purpose: Debug mode management command
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getConfig } from '../config.js';
|
|
7
|
+
import {
|
|
8
|
+
colors,
|
|
9
|
+
error,
|
|
10
|
+
info,
|
|
11
|
+
updateConfig
|
|
12
|
+
} from './helpers.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Sets debug mode
|
|
16
|
+
* Why: Enables detailed logging for troubleshooting
|
|
17
|
+
* @param {string} value - 'true', 'false', or 'status'
|
|
18
|
+
*/
|
|
19
|
+
export async function runSetDebug(value) {
|
|
20
|
+
if (!value) {
|
|
21
|
+
error('Please specify a value: claude-hooks --debug <true|false|status>');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const normalizedValue = value.toLowerCase();
|
|
26
|
+
|
|
27
|
+
// Handle status check
|
|
28
|
+
if (normalizedValue === 'status') {
|
|
29
|
+
try {
|
|
30
|
+
const config = await getConfig();
|
|
31
|
+
const isEnabled = config.system.debug || false;
|
|
32
|
+
console.log('');
|
|
33
|
+
info(`Debug mode: ${isEnabled ? colors.green + 'enabled' + colors.reset : colors.red + 'disabled' + colors.reset}`);
|
|
34
|
+
console.log('');
|
|
35
|
+
} catch (err) {
|
|
36
|
+
error(`Failed to check debug status: ${err.message}`);
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Validate and convert to boolean
|
|
42
|
+
if (normalizedValue !== 'true' && normalizedValue !== 'false') {
|
|
43
|
+
error('Invalid value. Use: true, false, or status');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const debugValue = normalizedValue === 'true';
|
|
48
|
+
|
|
49
|
+
await updateConfig('system.debug', debugValue, {
|
|
50
|
+
successMessage: (val) => `Debug mode ${val ? 'enabled' : 'disabled'}`
|
|
51
|
+
});
|
|
52
|
+
}
|