claude-git-hooks 2.4.0 ā 2.5.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 +262 -135
- package/README.md +158 -67
- package/bin/claude-hooks +452 -10
- package/lib/config.js +29 -0
- package/lib/hooks/pre-commit.js +2 -6
- package/lib/hooks/prepare-commit-msg.js +27 -4
- package/lib/utils/claude-client.js +148 -16
- package/lib/utils/file-operations.js +0 -102
- package/lib/utils/github-api.js +641 -0
- package/lib/utils/github-client.js +770 -0
- package/lib/utils/interactive-ui.js +314 -0
- package/lib/utils/mcp-setup.js +342 -0
- package/lib/utils/sanitize.js +180 -0
- package/lib/utils/task-id.js +425 -0
- package/package.json +4 -1
- package/templates/CREATE_GITHUB_PR.md +32 -0
- package/templates/config.example.json +41 -41
- package/templates/config.github.example.json +51 -0
- package/templates/presets/ai/PRE_COMMIT_GUIDELINES.md +18 -1
- package/templates/presets/ai/config.json +12 -12
- package/templates/presets/ai/preset.json +37 -42
- package/templates/presets/backend/ANALYSIS_PROMPT.md +23 -28
- package/templates/presets/backend/PRE_COMMIT_GUIDELINES.md +41 -3
- package/templates/presets/backend/config.json +12 -12
- package/templates/presets/database/config.json +12 -12
- package/templates/presets/default/config.json +12 -12
- package/templates/presets/frontend/config.json +12 -12
- package/templates/presets/fullstack/config.json +12 -12
- package/templates/settings.local.example.json +4 -0
package/bin/claude-hooks
CHANGED
|
@@ -12,6 +12,11 @@ import { executeClaude, extractJSON } from '../lib/utils/claude-client.js';
|
|
|
12
12
|
import { loadPrompt } from '../lib/utils/prompt-builder.js';
|
|
13
13
|
import { listPresets } from '../lib/utils/preset-loader.js';
|
|
14
14
|
import { getConfig } from '../lib/config.js';
|
|
15
|
+
import { getOrPromptTaskId, formatWithTaskId } from '../lib/utils/task-id.js';
|
|
16
|
+
import { createPullRequest, getReviewersForFiles, parseGitHubRepo, setupGitHubMcp, getGitHubMcpStatus } from '../lib/utils/github-client.js';
|
|
17
|
+
import { showPRPreview, promptConfirmation, promptMenu, showSuccess, showError, showInfo, showWarning, showSpinner, promptEditField } from '../lib/utils/interactive-ui.js';
|
|
18
|
+
import { setupGitHubMCP } from '../lib/utils/mcp-setup.js';
|
|
19
|
+
import logger from '../lib/utils/logger.js';
|
|
15
20
|
|
|
16
21
|
// Why: ES6 modules don't have __dirname, need to recreate it
|
|
17
22
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -452,6 +457,17 @@ async function install(args) {
|
|
|
452
457
|
warning('config.json not found - using defaults');
|
|
453
458
|
}
|
|
454
459
|
|
|
460
|
+
// Create settings.local.json for sensitive data (gitignored)
|
|
461
|
+
const settingsLocalPath = path.join(claudeDir, 'settings.local.json');
|
|
462
|
+
if (!fs.existsSync(settingsLocalPath)) {
|
|
463
|
+
const settingsLocalContent = {
|
|
464
|
+
"_comment": "Local settings - DO NOT COMMIT. This file is gitignored.",
|
|
465
|
+
"githubToken": ""
|
|
466
|
+
};
|
|
467
|
+
fs.writeFileSync(settingsLocalPath, JSON.stringify(settingsLocalContent, null, 2));
|
|
468
|
+
info('settings.local.json created (add your GitHub token here)');
|
|
469
|
+
}
|
|
470
|
+
|
|
455
471
|
// Configure Git
|
|
456
472
|
configureGit();
|
|
457
473
|
|
|
@@ -469,10 +485,14 @@ async function install(args) {
|
|
|
469
485
|
console.log(' šÆ Use presets: backend, frontend, fullstack, database, ai, default');
|
|
470
486
|
console.log(' š Enable parallel analysis: set subagents.enabled = true');
|
|
471
487
|
console.log(' š Enable debug mode: claude-hooks --debug true');
|
|
488
|
+
console.log('\nš GitHub PR Creation (v2.5.0+):');
|
|
489
|
+
console.log(' claude-hooks setup-github # Configure GitHub token for create-pr');
|
|
490
|
+
console.log(' claude-hooks create-pr main # Create PR with auto-generated metadata');
|
|
472
491
|
console.log('\nš Example config.json:');
|
|
473
492
|
console.log(' {');
|
|
474
493
|
console.log(' "preset": "backend",');
|
|
475
|
-
console.log(' "subagents": { "enabled": true, "model": "haiku", "batchSize": 3 }');
|
|
494
|
+
console.log(' "subagents": { "enabled": true, "model": "haiku", "batchSize": 3 },');
|
|
495
|
+
console.log(' "github": { "pr": { "reviewers": ["your-username"] } }');
|
|
476
496
|
console.log(' }');
|
|
477
497
|
console.log('\nFor more options: claude-hooks --help');
|
|
478
498
|
}
|
|
@@ -509,14 +529,12 @@ async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false
|
|
|
509
529
|
|
|
510
530
|
// v2.0.0+: Unix tools (sed, awk, grep, etc.) no longer needed (pure Node.js implementation)
|
|
511
531
|
|
|
512
|
-
// Check and install Claude CLI
|
|
513
|
-
await checkAndInstallClaude();
|
|
514
|
-
|
|
515
|
-
// Check Claude authentication (if not skipped)
|
|
532
|
+
// Check and install Claude CLI (skip if --skip-auth)
|
|
516
533
|
if (!skipAuth) {
|
|
534
|
+
await checkAndInstallClaude();
|
|
517
535
|
await checkClaudeAuth();
|
|
518
536
|
} else {
|
|
519
|
-
warning('Skipping Claude
|
|
537
|
+
warning('Skipping Claude CLI verification and authentication (--skip-auth)');
|
|
520
538
|
}
|
|
521
539
|
|
|
522
540
|
// Clear password from memory
|
|
@@ -530,9 +548,19 @@ function isWindows() {
|
|
|
530
548
|
}
|
|
531
549
|
|
|
532
550
|
// Get Claude command based on platform
|
|
533
|
-
// Why: On Windows,
|
|
551
|
+
// Why: On Windows, try native Claude first, then WSL as fallback
|
|
534
552
|
function getClaudeCommand() {
|
|
535
|
-
|
|
553
|
+
if (isWindows()) {
|
|
554
|
+
// Try native Windows Claude first
|
|
555
|
+
try {
|
|
556
|
+
execSync('claude --version', { stdio: 'ignore', timeout: 3000 });
|
|
557
|
+
return 'claude';
|
|
558
|
+
} catch (e) {
|
|
559
|
+
// Fallback to WSL
|
|
560
|
+
return 'wsl claude';
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return 'claude';
|
|
536
564
|
}
|
|
537
565
|
|
|
538
566
|
// Check if we need to install dependencies
|
|
@@ -629,7 +657,7 @@ function updateGitignore() {
|
|
|
629
657
|
|
|
630
658
|
const gitignorePath = '.gitignore';
|
|
631
659
|
const claudeEntries = [
|
|
632
|
-
'# Claude Git Hooks',
|
|
660
|
+
'# Claude Git Hooks (includes .claude/settings.local.json for tokens)',
|
|
633
661
|
'.claude/',
|
|
634
662
|
];
|
|
635
663
|
|
|
@@ -1017,6 +1045,392 @@ async function analyzeDiff(args) {
|
|
|
1017
1045
|
}
|
|
1018
1046
|
}
|
|
1019
1047
|
|
|
1048
|
+
// Create PR command (v2.5.0+ - Octokit-based)
|
|
1049
|
+
async function createPr(args) {
|
|
1050
|
+
logger.debug('create-pr', 'Starting create-pr command', { args });
|
|
1051
|
+
|
|
1052
|
+
if (!checkGitRepo()) {
|
|
1053
|
+
error('You are not in a Git repository.');
|
|
1054
|
+
logger.debug('create-pr', 'Not in a git repository, exiting');
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
try {
|
|
1059
|
+
// Load configuration
|
|
1060
|
+
logger.debug('create-pr', 'Loading configuration');
|
|
1061
|
+
const config = await getConfig();
|
|
1062
|
+
logger.debug('create-pr', 'Configuration loaded', {
|
|
1063
|
+
preset: config.preset,
|
|
1064
|
+
githubEnabled: config.github?.enabled,
|
|
1065
|
+
defaultBase: config.github?.pr?.defaultBase
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
// Import GitHub API module
|
|
1069
|
+
logger.debug('create-pr', 'Importing GitHub API modules');
|
|
1070
|
+
const { createPullRequest, GitHubAPIError, validateToken, findExistingPR } = await import('../lib/utils/github-api.js');
|
|
1071
|
+
const { parseGitHubRepo } = await import('../lib/utils/github-client.js');
|
|
1072
|
+
|
|
1073
|
+
showInfo('š Creating Pull Request...');
|
|
1074
|
+
console.log('');
|
|
1075
|
+
|
|
1076
|
+
// Step 1: Validate GitHub token
|
|
1077
|
+
logger.debug('create-pr', 'Step 1: Validating GitHub token');
|
|
1078
|
+
const tokenValidation = await validateToken();
|
|
1079
|
+
if (!tokenValidation.valid) {
|
|
1080
|
+
logger.error('create-pr', 'GitHub authentication failed', { error: tokenValidation.error });
|
|
1081
|
+
showError('GitHub authentication failed');
|
|
1082
|
+
console.log('');
|
|
1083
|
+
console.log('Please configure your GitHub token:');
|
|
1084
|
+
console.log(' Option 1: Set GITHUB_TOKEN environment variable');
|
|
1085
|
+
console.log(' Option 2: Add token to .claude/settings.local.json:');
|
|
1086
|
+
console.log(' { "githubToken": "ghp_your_token_here" }');
|
|
1087
|
+
console.log(' Option 3: Run: claude-hooks setup-github');
|
|
1088
|
+
console.log('');
|
|
1089
|
+
process.exit(1);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
logger.debug('create-pr', 'Token validation successful', {
|
|
1093
|
+
user: tokenValidation.user,
|
|
1094
|
+
hasRepoScope: tokenValidation.hasRepoScope,
|
|
1095
|
+
scopes: tokenValidation.scopes
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
showSuccess(`Authenticated as: ${tokenValidation.user}`);
|
|
1099
|
+
if (!tokenValidation.hasRepoScope) {
|
|
1100
|
+
showWarning('Token may lack "repo" scope - PR creation might fail');
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// Step 2: Get or prompt for task-id (with config for pattern)
|
|
1104
|
+
logger.debug('create-pr', 'Step 2: Getting or prompting for task-id');
|
|
1105
|
+
const taskId = await getOrPromptTaskId({
|
|
1106
|
+
prompt: true, // DO prompt for PRs (unlike commit messages)
|
|
1107
|
+
required: false, // Allow skipping
|
|
1108
|
+
config: config // Pass config for custom pattern
|
|
1109
|
+
});
|
|
1110
|
+
logger.debug('create-pr', 'Task ID determined', { taskId });
|
|
1111
|
+
|
|
1112
|
+
// Step 3: Parse arguments and determine base branch
|
|
1113
|
+
logger.debug('create-pr', 'Step 3: Parsing arguments and determining base branch', { args });
|
|
1114
|
+
let baseBranchArg = args[0];
|
|
1115
|
+
if (baseBranchArg && /^[A-Z]{2,10}-\d+$/i.test(baseBranchArg)) {
|
|
1116
|
+
baseBranchArg = args[1];
|
|
1117
|
+
}
|
|
1118
|
+
const baseBranch = baseBranchArg || config.github?.pr?.defaultBase || 'develop';
|
|
1119
|
+
logger.debug('create-pr', 'Base branch determined', { baseBranch, fromConfig: !baseBranchArg });
|
|
1120
|
+
|
|
1121
|
+
// Step 4: Get current branch and repo info
|
|
1122
|
+
logger.debug('create-pr', 'Step 4: Getting current branch and repo info');
|
|
1123
|
+
const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
|
|
1124
|
+
if (!currentBranch) {
|
|
1125
|
+
logger.error('create-pr', 'Could not determine current branch');
|
|
1126
|
+
error('Could not determine current branch');
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const repoInfo = parseGitHubRepo();
|
|
1131
|
+
logger.debug('create-pr', 'Repository and branch info', {
|
|
1132
|
+
owner: repoInfo.owner,
|
|
1133
|
+
repo: repoInfo.repo,
|
|
1134
|
+
currentBranch,
|
|
1135
|
+
baseBranch
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
showInfo(`Repository: ${repoInfo.fullName}`);
|
|
1139
|
+
showInfo(`Branch: ${currentBranch} ā ${baseBranch}`);
|
|
1140
|
+
|
|
1141
|
+
// Step 5: Check for existing PR
|
|
1142
|
+
logger.debug('create-pr', 'Step 5: Checking for existing PR');
|
|
1143
|
+
const existingPR = await findExistingPR({
|
|
1144
|
+
owner: repoInfo.owner,
|
|
1145
|
+
repo: repoInfo.repo,
|
|
1146
|
+
head: currentBranch,
|
|
1147
|
+
base: baseBranch
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
if (existingPR) {
|
|
1151
|
+
logger.debug('create-pr', 'Existing PR found, exiting', {
|
|
1152
|
+
prNumber: existingPR.number,
|
|
1153
|
+
prUrl: existingPR.html_url
|
|
1154
|
+
});
|
|
1155
|
+
showWarning(`A PR already exists for this branch: #${existingPR.number}`);
|
|
1156
|
+
console.log(` ${existingPR.html_url}`);
|
|
1157
|
+
console.log('');
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
logger.debug('create-pr', 'No existing PR found, continuing');
|
|
1162
|
+
|
|
1163
|
+
// Step 6: Update remote and check for differences
|
|
1164
|
+
logger.debug('create-pr', 'Step 6: Fetching latest changes from remote');
|
|
1165
|
+
execSync('git fetch', { stdio: 'ignore' });
|
|
1166
|
+
const compareWith = `origin/${baseBranch}...HEAD`;
|
|
1167
|
+
|
|
1168
|
+
try {
|
|
1169
|
+
execSync(`git rev-parse --verify origin/${baseBranch}`, { stdio: 'ignore' });
|
|
1170
|
+
} catch (e) {
|
|
1171
|
+
error(`Base branch origin/${baseBranch} does not exist`);
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
let diffFiles;
|
|
1176
|
+
try {
|
|
1177
|
+
diffFiles = execSync(`git diff ${compareWith} --name-only`, { encoding: 'utf8' }).trim();
|
|
1178
|
+
if (!diffFiles) {
|
|
1179
|
+
showWarning('No differences with remote branch. Nothing to create a PR for.');
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
} catch (e) {
|
|
1183
|
+
error('Error getting differences: ' + e.message);
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const filesArray = diffFiles.split('\n').filter(f => f.trim());
|
|
1188
|
+
logger.debug('create-pr', 'Modified files detected', {
|
|
1189
|
+
fileCount: filesArray.length,
|
|
1190
|
+
files: filesArray
|
|
1191
|
+
});
|
|
1192
|
+
showInfo(`Found ${filesArray.length} modified file(s)`);
|
|
1193
|
+
|
|
1194
|
+
// Step 7: Generate PR metadata with Claude (reuse analyze-diff logic)
|
|
1195
|
+
logger.debug('create-pr', 'Step 7: Generating PR metadata with Claude');
|
|
1196
|
+
let fullDiff, commits;
|
|
1197
|
+
try {
|
|
1198
|
+
fullDiff = execSync(`git diff ${compareWith}`, { encoding: 'utf8' });
|
|
1199
|
+
commits = execSync(`git log origin/${baseBranch}..HEAD --oneline`, { encoding: 'utf8' }).trim();
|
|
1200
|
+
} catch (e) {
|
|
1201
|
+
error('Error getting diff or commits: ' + e.message);
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
const truncatedDiff = fullDiff.length > 50000
|
|
1206
|
+
? fullDiff.substring(0, 50000) + '\n... (truncated)'
|
|
1207
|
+
: fullDiff;
|
|
1208
|
+
|
|
1209
|
+
const contextDescription = `${currentBranch} vs origin/${baseBranch}`;
|
|
1210
|
+
const prompt = await loadPrompt('ANALYZE_DIFF.md', {
|
|
1211
|
+
CONTEXT_DESCRIPTION: contextDescription,
|
|
1212
|
+
SUBAGENT_INSTRUCTION: '',
|
|
1213
|
+
COMMITS: commits,
|
|
1214
|
+
DIFF_FILES: diffFiles,
|
|
1215
|
+
FULL_DIFF: truncatedDiff
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
showInfo('Generating PR metadata with Claude...');
|
|
1219
|
+
logger.debug('create-pr', 'Calling Claude with prompt', { promptLength: prompt.length });
|
|
1220
|
+
const response = await executeClaude(prompt, { timeout: 180000 });
|
|
1221
|
+
logger.debug('create-pr', 'Claude response received', { responseLength: response.length });
|
|
1222
|
+
|
|
1223
|
+
const analysisResult = extractJSON(response);
|
|
1224
|
+
logger.debug('create-pr', 'Analysis result extracted', {
|
|
1225
|
+
hasResult: !!analysisResult,
|
|
1226
|
+
hasPrTitle: !!analysisResult?.prTitle
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
if (!analysisResult || !analysisResult.prTitle) {
|
|
1230
|
+
logger.error('create-pr', 'Failed to generate PR metadata from analysis', { analysisResult });
|
|
1231
|
+
error('Failed to generate PR metadata from analysis');
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// Step 8: Prepare PR data
|
|
1236
|
+
logger.debug('create-pr', 'Step 8: Preparing PR data');
|
|
1237
|
+
let prTitle = analysisResult.prTitle;
|
|
1238
|
+
if (taskId) {
|
|
1239
|
+
prTitle = formatWithTaskId(prTitle, taskId);
|
|
1240
|
+
logger.debug('create-pr', 'Task ID added to title', { prTitle });
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
const prBody = analysisResult.prDescription || analysisResult.description || '';
|
|
1244
|
+
logger.debug('create-pr', 'PR title and body prepared', {
|
|
1245
|
+
titleLength: prTitle.length,
|
|
1246
|
+
bodyLength: prBody.length
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
// Step 9: Get labels from preset
|
|
1250
|
+
logger.debug('create-pr', 'Step 9: Getting labels from preset');
|
|
1251
|
+
let labels = [];
|
|
1252
|
+
if (config.preset && config.github?.pr?.labelRules) {
|
|
1253
|
+
labels = config.github.pr.labelRules[config.preset] || [];
|
|
1254
|
+
}
|
|
1255
|
+
if (analysisResult.breakingChanges) {
|
|
1256
|
+
labels.push('breaking-change');
|
|
1257
|
+
}
|
|
1258
|
+
logger.debug('create-pr', 'Labels determined', { labels, preset: config.preset });
|
|
1259
|
+
|
|
1260
|
+
// Step 10: Get reviewers from CODEOWNERS and config
|
|
1261
|
+
logger.debug('create-pr', 'Step 10: Getting reviewers from CODEOWNERS and config');
|
|
1262
|
+
const reviewers = await getReviewersForFiles(filesArray, config.github?.pr);
|
|
1263
|
+
logger.debug('create-pr', 'Reviewers determined', { reviewers, sources: 'CODEOWNERS + config' });
|
|
1264
|
+
|
|
1265
|
+
// Step 11: Show PR preview
|
|
1266
|
+
const prData = {
|
|
1267
|
+
title: prTitle,
|
|
1268
|
+
body: prBody,
|
|
1269
|
+
head: currentBranch,
|
|
1270
|
+
base: baseBranch,
|
|
1271
|
+
labels,
|
|
1272
|
+
reviewers
|
|
1273
|
+
};
|
|
1274
|
+
|
|
1275
|
+
showPRPreview(prData);
|
|
1276
|
+
|
|
1277
|
+
// Step 12: Prompt for confirmation
|
|
1278
|
+
const action = await promptMenu(
|
|
1279
|
+
'What would you like to do?',
|
|
1280
|
+
[
|
|
1281
|
+
{ key: 'c', label: 'Create PR' },
|
|
1282
|
+
{ key: 'x', label: 'Cancel' }
|
|
1283
|
+
],
|
|
1284
|
+
'c'
|
|
1285
|
+
);
|
|
1286
|
+
|
|
1287
|
+
if (action === 'x') {
|
|
1288
|
+
showInfo('PR creation cancelled');
|
|
1289
|
+
|
|
1290
|
+
// Save metadata for later use
|
|
1291
|
+
const outputDir = '.claude/out';
|
|
1292
|
+
if (!fs.existsSync(outputDir)) {
|
|
1293
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
1294
|
+
}
|
|
1295
|
+
const outputFile = path.join(outputDir, 'pr-metadata.json');
|
|
1296
|
+
fs.writeFileSync(outputFile, JSON.stringify(prData, null, 2));
|
|
1297
|
+
showInfo(`PR metadata saved to ${outputFile}`);
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// Step 13: Create PR via Octokit
|
|
1302
|
+
logger.debug('create-pr', 'Step 13: Creating PR via Octokit');
|
|
1303
|
+
showInfo('Creating pull request on GitHub...');
|
|
1304
|
+
|
|
1305
|
+
try {
|
|
1306
|
+
logger.debug('create-pr', 'Calling createPullRequest API', {
|
|
1307
|
+
owner: repoInfo.owner,
|
|
1308
|
+
repo: repoInfo.repo,
|
|
1309
|
+
head: prData.head,
|
|
1310
|
+
base: prData.base
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
const result = await createPullRequest({
|
|
1314
|
+
owner: repoInfo.owner,
|
|
1315
|
+
repo: repoInfo.repo,
|
|
1316
|
+
title: prData.title,
|
|
1317
|
+
body: prData.body,
|
|
1318
|
+
head: prData.head,
|
|
1319
|
+
base: prData.base,
|
|
1320
|
+
draft: false,
|
|
1321
|
+
labels: prData.labels,
|
|
1322
|
+
reviewers: prData.reviewers
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
logger.debug('create-pr', 'PR created successfully', {
|
|
1326
|
+
prNumber: result.number,
|
|
1327
|
+
prUrl: result.html_url
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
console.log('');
|
|
1331
|
+
showSuccess('Pull request created successfully!');
|
|
1332
|
+
console.log('');
|
|
1333
|
+
console.log(` PR #${result.number}: ${result.html_url}`);
|
|
1334
|
+
console.log('');
|
|
1335
|
+
|
|
1336
|
+
if (result.reviewers.length > 0) {
|
|
1337
|
+
showInfo(`Reviewers requested: ${result.reviewers.join(', ')}`);
|
|
1338
|
+
}
|
|
1339
|
+
if (result.labels.length > 0) {
|
|
1340
|
+
showInfo(`Labels added: ${result.labels.join(', ')}`);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
} catch (apiError) {
|
|
1344
|
+
logger.error('create-pr', 'Failed to create pull request', apiError);
|
|
1345
|
+
showError('Failed to create pull request');
|
|
1346
|
+
console.error('');
|
|
1347
|
+
console.error(` ${apiError.message}`);
|
|
1348
|
+
|
|
1349
|
+
if (apiError.context?.suggestion) {
|
|
1350
|
+
console.error('');
|
|
1351
|
+
console.error(` š” ${apiError.context.suggestion}`);
|
|
1352
|
+
}
|
|
1353
|
+
console.error('');
|
|
1354
|
+
|
|
1355
|
+
// Save PR metadata for manual creation or retry
|
|
1356
|
+
const outputDir = '.claude/out';
|
|
1357
|
+
if (!fs.existsSync(outputDir)) {
|
|
1358
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
1359
|
+
}
|
|
1360
|
+
const outputFile = path.join(outputDir, 'pr-metadata.json');
|
|
1361
|
+
fs.writeFileSync(outputFile, JSON.stringify({
|
|
1362
|
+
...prData,
|
|
1363
|
+
error: apiError.message,
|
|
1364
|
+
timestamp: new Date().toISOString()
|
|
1365
|
+
}, null, 2));
|
|
1366
|
+
|
|
1367
|
+
logger.debug('create-pr', 'PR metadata saved', { outputFile });
|
|
1368
|
+
showInfo(`PR metadata saved to ${outputFile}`);
|
|
1369
|
+
showInfo('You can create the PR manually using this data');
|
|
1370
|
+
|
|
1371
|
+
process.exit(1);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
} catch (err) {
|
|
1375
|
+
logger.error('create-pr', 'Error creating PR', err);
|
|
1376
|
+
showError('Error creating PR: ' + err.message);
|
|
1377
|
+
|
|
1378
|
+
if (err.context) {
|
|
1379
|
+
logger.debug('create-pr', 'Error context', err.context);
|
|
1380
|
+
console.error('Context:', JSON.stringify(err.context, null, 2));
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
process.exit(1);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// Setup GitHub authentication
|
|
1388
|
+
async function setupGitHub() {
|
|
1389
|
+
const { validateToken } = await import('../lib/utils/github-api.js');
|
|
1390
|
+
|
|
1391
|
+
console.log('');
|
|
1392
|
+
info('GitHub Authentication Setup');
|
|
1393
|
+
console.log('');
|
|
1394
|
+
|
|
1395
|
+
// Check existing token
|
|
1396
|
+
try {
|
|
1397
|
+
const validation = await validateToken();
|
|
1398
|
+
if (validation.valid) {
|
|
1399
|
+
success(`Already authenticated as: ${validation.user}`);
|
|
1400
|
+
console.log(` Scopes: ${validation.scopes.join(', ')}`);
|
|
1401
|
+
|
|
1402
|
+
if (!validation.hasRepoScope) {
|
|
1403
|
+
warning('Token lacks "repo" scope - PR creation may fail');
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
console.log('');
|
|
1407
|
+
info('To use a different token, edit .claude/settings.local.json');
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
} catch (e) {
|
|
1411
|
+
// No token configured, continue with setup
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
console.log('No GitHub token found. You have several options:');
|
|
1415
|
+
console.log('');
|
|
1416
|
+
console.log('Option 1: Create .claude/settings.local.json');
|
|
1417
|
+
console.log(' {');
|
|
1418
|
+
console.log(' "githubToken": "ghp_your_token_here"');
|
|
1419
|
+
console.log(' }');
|
|
1420
|
+
console.log('');
|
|
1421
|
+
console.log('Option 2: Set environment variable');
|
|
1422
|
+
console.log(' export GITHUB_TOKEN="ghp_your_token_here"');
|
|
1423
|
+
console.log('');
|
|
1424
|
+
console.log('Option 3: Run setup-mcp (if you also want MCP features)');
|
|
1425
|
+
console.log(' claude-hooks setup-mcp');
|
|
1426
|
+
console.log('');
|
|
1427
|
+
console.log('To create a token:');
|
|
1428
|
+
console.log(' 1. Go to https://github.com/settings/tokens/new');
|
|
1429
|
+
console.log(' 2. Select scopes: repo, read:org');
|
|
1430
|
+
console.log(' 3. Generate and copy the token');
|
|
1431
|
+
console.log('');
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1020
1434
|
// Comando status
|
|
1021
1435
|
function status() {
|
|
1022
1436
|
if (!checkGitRepo()) {
|
|
@@ -1165,6 +1579,8 @@ Commands:
|
|
|
1165
1579
|
disable [hook] Disable hooks (all or one specific)
|
|
1166
1580
|
status Show the status of hooks
|
|
1167
1581
|
analyze-diff [base] Analyze differences between branches and generate PR info
|
|
1582
|
+
create-pr [base] Create pull request with auto-generated metadata and reviewers
|
|
1583
|
+
setup-github Setup GitHub login (required for create-pr)
|
|
1168
1584
|
presets List all available presets
|
|
1169
1585
|
--set-preset <name> Set the active preset
|
|
1170
1586
|
preset current Show the current active preset
|
|
@@ -1184,6 +1600,9 @@ Examples:
|
|
|
1184
1600
|
claude-hooks enable # Enable all hooks
|
|
1185
1601
|
claude-hooks status # View current status
|
|
1186
1602
|
claude-hooks analyze-diff main # Analyze differences with main
|
|
1603
|
+
claude-hooks setup-github # Configure GitHub authentication for PR creation
|
|
1604
|
+
claude-hooks setup-mcp # Setup GitHub MCP (one-time setup)
|
|
1605
|
+
claude-hooks create-pr develop # Create PR targeting develop branch
|
|
1187
1606
|
claude-hooks presets # List available presets
|
|
1188
1607
|
claude-hooks --set-preset backend # Set backend preset
|
|
1189
1608
|
claude-hooks preset current # Show current preset
|
|
@@ -1202,6 +1621,20 @@ Analyze-diff use case:
|
|
|
1202
1621
|
ā PR Description: "## Summary\n- Added JWT authentication..."
|
|
1203
1622
|
ā Suggested branch: "feature/user-authentication"
|
|
1204
1623
|
|
|
1624
|
+
Create-pr use case (v2.5.0+):
|
|
1625
|
+
claude-hooks create-pr develop # Create PR targeting develop:
|
|
1626
|
+
ā Validates GitHub token
|
|
1627
|
+
ā Extracts task-id from branch (IX-123, #456, LIN-123)
|
|
1628
|
+
ā Analyzes diff and generates PR metadata with Claude
|
|
1629
|
+
ā Creates PR directly via GitHub API (Octokit)
|
|
1630
|
+
ā Adds labels based on preset
|
|
1631
|
+
ā Returns PR URL
|
|
1632
|
+
|
|
1633
|
+
Token configuration:
|
|
1634
|
+
ā .claude/settings.local.json (recommended, gitignored)
|
|
1635
|
+
ā GITHUB_TOKEN environment variable
|
|
1636
|
+
ā Claude Desktop config (auto-detected)
|
|
1637
|
+
|
|
1205
1638
|
Presets (v2.3.0+):
|
|
1206
1639
|
Built-in tech-stack specific configurations:
|
|
1207
1640
|
- backend: Spring Boot + SQL Server (.java, .xml, .yml)
|
|
@@ -1310,7 +1743,7 @@ async function updateConfig(propertyPath, value, options = {}) {
|
|
|
1310
1743
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
1311
1744
|
|
|
1312
1745
|
// Show success message
|
|
1313
|
-
const message = successMessage ? successMessage(value) : 'Configuration updated';
|
|
1746
|
+
const message = successMessage ? await successMessage(value) : 'Configuration updated';
|
|
1314
1747
|
success(message);
|
|
1315
1748
|
info(`Configuration saved to ${configPath}`);
|
|
1316
1749
|
} catch (err) {
|
|
@@ -1472,6 +1905,15 @@ async function main() {
|
|
|
1472
1905
|
case 'analyze-diff':
|
|
1473
1906
|
await analyzeDiff(args.slice(1));
|
|
1474
1907
|
break;
|
|
1908
|
+
case 'create-pr':
|
|
1909
|
+
await createPr(args.slice(1));
|
|
1910
|
+
break;
|
|
1911
|
+
case 'setup-mcp':
|
|
1912
|
+
await setupGitHubMCP();
|
|
1913
|
+
break;
|
|
1914
|
+
case 'setup-github':
|
|
1915
|
+
await setupGitHub();
|
|
1916
|
+
break;
|
|
1475
1917
|
case 'presets':
|
|
1476
1918
|
await showPresets();
|
|
1477
1919
|
break;
|
package/lib/config.js
CHANGED
|
@@ -38,6 +38,11 @@ const defaults = {
|
|
|
38
38
|
commitMessage: {
|
|
39
39
|
autoKeyword: 'auto', // Keyword to trigger auto-generation
|
|
40
40
|
timeout: 300000,
|
|
41
|
+
// Task-ID detection pattern (from branch name)
|
|
42
|
+
// Default: 1-3 uppercase letters + separator + 3-5 digits
|
|
43
|
+
// Examples: "ABC-12345", "IX-123", "DE 4567"
|
|
44
|
+
// Regex is case-insensitive and extracted from branch name
|
|
45
|
+
taskIdPattern: '([A-Z]{1,3}[-\\s]\\d{3,5})'
|
|
41
46
|
},
|
|
42
47
|
|
|
43
48
|
// Subagent configuration (parallel analysis)
|
|
@@ -56,6 +61,7 @@ const defaults = {
|
|
|
56
61
|
analyzeDiff: 'ANALYZE_DIFF.md', // PR analysis prompt
|
|
57
62
|
resolution: 'CLAUDE_RESOLUTION_PROMPT.md', // Issue resolution prompt
|
|
58
63
|
subagentInstruction: 'SUBAGENT_INSTRUCTION.md', // Parallel analysis instruction
|
|
64
|
+
createGithubPR: 'CREATE_GITHUB_PR.md', // GitHub PR creation via MCP
|
|
59
65
|
},
|
|
60
66
|
|
|
61
67
|
// Output file paths (relative to repo root)
|
|
@@ -76,6 +82,29 @@ const defaults = {
|
|
|
76
82
|
git: {
|
|
77
83
|
diffFilter: 'ACM', // Added, Copied, Modified (excludes Deleted)
|
|
78
84
|
},
|
|
85
|
+
|
|
86
|
+
// GitHub integration (v2.5.0+)
|
|
87
|
+
github: {
|
|
88
|
+
enabled: true, // Changed from false - enable by default since Octokit works without MCP
|
|
89
|
+
|
|
90
|
+
// Pull Request configuration
|
|
91
|
+
pr: {
|
|
92
|
+
defaultBase: 'develop', // Default base branch for PRs
|
|
93
|
+
|
|
94
|
+
// Reviewers (usernames without @)
|
|
95
|
+
reviewers: [], // Example: ["juan.perez", "maria.garcia"]
|
|
96
|
+
|
|
97
|
+
// Labels by preset
|
|
98
|
+
labelRules: {
|
|
99
|
+
backend: ['backend', 'java'],
|
|
100
|
+
frontend: ['frontend', 'react'],
|
|
101
|
+
fullstack: ['fullstack'],
|
|
102
|
+
database: ['database', 'sql'],
|
|
103
|
+
ai: ['ai', 'tooling'],
|
|
104
|
+
default: []
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
79
108
|
};
|
|
80
109
|
|
|
81
110
|
/**
|
package/lib/hooks/pre-commit.js
CHANGED
|
@@ -27,8 +27,6 @@ import {
|
|
|
27
27
|
} from '../utils/git-operations.js';
|
|
28
28
|
import {
|
|
29
29
|
filterFiles,
|
|
30
|
-
filterSkipAnalysis,
|
|
31
|
-
readFile
|
|
32
30
|
} from '../utils/file-operations.js';
|
|
33
31
|
import { analyzeCode, analyzeCodeParallel, chunkArray } from '../utils/claude-client.js';
|
|
34
32
|
import { buildAnalysisPrompt } from '../utils/prompt-builder.js';
|
|
@@ -282,6 +280,8 @@ const main = async () => {
|
|
|
282
280
|
logger.warning('Consider splitting the commit into smaller parts');
|
|
283
281
|
process.exit(0);
|
|
284
282
|
}
|
|
283
|
+
|
|
284
|
+
logger.info("test log");
|
|
285
285
|
|
|
286
286
|
// Step 3: Build file data for prompt
|
|
287
287
|
logger.debug('pre-commit - main', 'Building file data for analysis');
|
|
@@ -290,9 +290,6 @@ const main = async () => {
|
|
|
290
290
|
// Get diff
|
|
291
291
|
let diff = getFileDiff(filePath);
|
|
292
292
|
|
|
293
|
-
// Apply SKIP_ANALYSIS filtering
|
|
294
|
-
diff = filterSkipAnalysis(diff);
|
|
295
|
-
|
|
296
293
|
// Check if new file
|
|
297
294
|
const isNew = isNewFile(filePath);
|
|
298
295
|
|
|
@@ -300,7 +297,6 @@ const main = async () => {
|
|
|
300
297
|
let content = null;
|
|
301
298
|
if (isNew) {
|
|
302
299
|
content = await getFileContentFromStaging(filePath);
|
|
303
|
-
content = filterSkipAnalysis(content);
|
|
304
300
|
}
|
|
305
301
|
|
|
306
302
|
return {
|