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.
@@ -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
+ }