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
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: interactive-ui.js
|
|
3
|
+
* Purpose: Interactive UI helpers for command-line prompts
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - PR preview with colored output
|
|
7
|
+
* - Confirmation prompts
|
|
8
|
+
* - Field editing
|
|
9
|
+
* - Keyboard navigation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import readline from 'readline';
|
|
13
|
+
import logger from './logger.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Show PR preview in formatted box
|
|
17
|
+
* Why: Give user visual preview before creating PR
|
|
18
|
+
*
|
|
19
|
+
* @param {Object} prData - PR data to preview
|
|
20
|
+
* @param {string} prData.title - PR title
|
|
21
|
+
* @param {string} prData.body - PR description
|
|
22
|
+
* @param {string} prData.base - Base branch
|
|
23
|
+
* @param {string} prData.head - Head branch
|
|
24
|
+
* @param {Array<string>} prData.labels - Labels
|
|
25
|
+
* @param {Array<string>} prData.reviewers - Reviewers
|
|
26
|
+
*/
|
|
27
|
+
export const showPRPreview = (prData) => {
|
|
28
|
+
const { title, body, base, head, labels = [], reviewers = [] } = prData;
|
|
29
|
+
|
|
30
|
+
console.log('');
|
|
31
|
+
console.log('┌───────────────────────────────────────────────────────────────┐');
|
|
32
|
+
console.log(' PR Preview');
|
|
33
|
+
console.log('├───────────────────────────────────────────────────────────────┤');
|
|
34
|
+
console.log(` Title: ${truncate(title, 55)}`);
|
|
35
|
+
console.log(` From: ${truncate(head, 55)}`);
|
|
36
|
+
console.log(` To: ${truncate(base, 55)}`);
|
|
37
|
+
|
|
38
|
+
if (labels.length > 0) {
|
|
39
|
+
console.log(` Labels: ${truncate(labels.join(', '), 53)}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (reviewers.length > 0) {
|
|
43
|
+
console.log(` Reviewers: ${truncate(reviewers.join(', '), 50)}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log('├───────────────────────────────────────────────────────────────┤');
|
|
47
|
+
console.log(' Description:');
|
|
48
|
+
|
|
49
|
+
// Show first 5 lines of body
|
|
50
|
+
const bodyLines = body.split('\n').slice(0, 5);
|
|
51
|
+
bodyLines.forEach(line => {
|
|
52
|
+
console.log(` ${truncate(line, 61)}`);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (body.split('\n').length > 5) {
|
|
56
|
+
console.log(' ... (truncated)');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log('└───────────────────────────────────────────────────────────────┘');
|
|
60
|
+
console.log('');
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Truncate string to fit in box
|
|
65
|
+
* @private
|
|
66
|
+
*/
|
|
67
|
+
const truncate = (str, maxLen) => {
|
|
68
|
+
const padded = str.padEnd(maxLen, ' ');
|
|
69
|
+
return padded.length > maxLen ? padded.substring(0, maxLen - 3) + '...' : padded;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Prompt user for confirmation with custom message
|
|
74
|
+
* Why: Get yes/no confirmation before destructive operations
|
|
75
|
+
*
|
|
76
|
+
* @param {string} message - Question to ask
|
|
77
|
+
* @param {boolean} defaultValue - Default if user presses Enter (default: true)
|
|
78
|
+
* @returns {Promise<boolean>} - True if confirmed, false otherwise
|
|
79
|
+
*/
|
|
80
|
+
export const promptConfirmation = async (message, defaultValue = true) => {
|
|
81
|
+
const rl = readline.createInterface({
|
|
82
|
+
input: process.stdin,
|
|
83
|
+
output: process.stdout
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return new Promise((resolve) => {
|
|
87
|
+
const defaultText = defaultValue ? 'Y/n' : 'y/N';
|
|
88
|
+
const promptMessage = `${message} (${defaultText}): `;
|
|
89
|
+
|
|
90
|
+
rl.question(promptMessage, (answer) => {
|
|
91
|
+
rl.close();
|
|
92
|
+
|
|
93
|
+
const trimmed = answer.trim().toLowerCase();
|
|
94
|
+
|
|
95
|
+
// If empty, use default
|
|
96
|
+
if (!trimmed) {
|
|
97
|
+
logger.debug('interactive-ui - promptConfirmation', 'Using default', { defaultValue });
|
|
98
|
+
resolve(defaultValue);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check yes/no
|
|
103
|
+
const isYes = ['y', 'yes', 'si', 's'].includes(trimmed);
|
|
104
|
+
const isNo = ['n', 'no'].includes(trimmed);
|
|
105
|
+
|
|
106
|
+
if (isYes) {
|
|
107
|
+
logger.debug('interactive-ui - promptConfirmation', 'User confirmed');
|
|
108
|
+
resolve(true);
|
|
109
|
+
} else if (isNo) {
|
|
110
|
+
logger.debug('interactive-ui - promptConfirmation', 'User declined');
|
|
111
|
+
resolve(false);
|
|
112
|
+
} else {
|
|
113
|
+
// Invalid input, use default
|
|
114
|
+
logger.debug('interactive-ui - promptConfirmation', 'Invalid input, using default', {
|
|
115
|
+
answer: trimmed,
|
|
116
|
+
defaultValue
|
|
117
|
+
});
|
|
118
|
+
resolve(defaultValue);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Prompt user to edit a field value
|
|
126
|
+
* Why: Allow user to modify PR fields before creation
|
|
127
|
+
*
|
|
128
|
+
* @param {string} fieldName - Name of field (e.g., "Title", "Description")
|
|
129
|
+
* @param {string} currentValue - Current value
|
|
130
|
+
* @returns {Promise<string>} - Updated value (or current if user skips)
|
|
131
|
+
*/
|
|
132
|
+
export const promptEditField = async (fieldName, currentValue) => {
|
|
133
|
+
const rl = readline.createInterface({
|
|
134
|
+
input: process.stdin,
|
|
135
|
+
output: process.stdout
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return new Promise((resolve) => {
|
|
139
|
+
console.log(`\nCurrent ${fieldName}:`);
|
|
140
|
+
console.log(currentValue);
|
|
141
|
+
console.log('');
|
|
142
|
+
|
|
143
|
+
const promptMessage = `New ${fieldName} (press Enter to keep current): `;
|
|
144
|
+
|
|
145
|
+
rl.question(promptMessage, (answer) => {
|
|
146
|
+
rl.close();
|
|
147
|
+
|
|
148
|
+
const trimmed = answer.trim();
|
|
149
|
+
|
|
150
|
+
if (!trimmed) {
|
|
151
|
+
logger.debug('interactive-ui - promptEditField', 'Keeping current value', { fieldName });
|
|
152
|
+
resolve(currentValue);
|
|
153
|
+
} else {
|
|
154
|
+
logger.debug('interactive-ui - promptEditField', 'Updated value', { fieldName });
|
|
155
|
+
resolve(trimmed);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Show menu with options and get user choice
|
|
163
|
+
* Why: Present multiple options to user
|
|
164
|
+
*
|
|
165
|
+
* @param {string} message - Menu message
|
|
166
|
+
* @param {Array<{key: string, label: string}>} options - Menu options
|
|
167
|
+
* @param {string} defaultKey - Default option key if user presses Enter
|
|
168
|
+
* @returns {Promise<string>} - Selected option key
|
|
169
|
+
*/
|
|
170
|
+
export const promptMenu = async (message, options, defaultKey = null) => {
|
|
171
|
+
const rl = readline.createInterface({
|
|
172
|
+
input: process.stdin,
|
|
173
|
+
output: process.stdout
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return new Promise((resolve) => {
|
|
177
|
+
console.log('');
|
|
178
|
+
console.log(message);
|
|
179
|
+
console.log('');
|
|
180
|
+
|
|
181
|
+
// Display options
|
|
182
|
+
options.forEach(opt => {
|
|
183
|
+
const isDefault = opt.key === defaultKey;
|
|
184
|
+
const marker = isDefault ? '→' : ' ';
|
|
185
|
+
console.log(` ${marker} [${opt.key}] ${opt.label}`);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
console.log('');
|
|
189
|
+
|
|
190
|
+
const defaultText = defaultKey ? ` (default: ${defaultKey})` : '';
|
|
191
|
+
const promptMessage = `Choose an option${defaultText}: `;
|
|
192
|
+
|
|
193
|
+
rl.question(promptMessage, (answer) => {
|
|
194
|
+
rl.close();
|
|
195
|
+
|
|
196
|
+
const trimmed = answer.trim().toLowerCase();
|
|
197
|
+
|
|
198
|
+
// If empty, use default
|
|
199
|
+
if (!trimmed && defaultKey) {
|
|
200
|
+
logger.debug('interactive-ui - promptMenu', 'Using default', { defaultKey });
|
|
201
|
+
resolve(defaultKey);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check if valid option
|
|
206
|
+
const selectedOption = options.find(opt => opt.key.toLowerCase() === trimmed);
|
|
207
|
+
|
|
208
|
+
if (selectedOption) {
|
|
209
|
+
logger.debug('interactive-ui - promptMenu', 'Option selected', { key: selectedOption.key });
|
|
210
|
+
resolve(selectedOption.key);
|
|
211
|
+
} else {
|
|
212
|
+
// Invalid, use default or first option
|
|
213
|
+
const fallback = defaultKey || options[0]?.key;
|
|
214
|
+
logger.debug('interactive-ui - promptMenu', 'Invalid option, using fallback', {
|
|
215
|
+
answer: trimmed,
|
|
216
|
+
fallback
|
|
217
|
+
});
|
|
218
|
+
resolve(fallback);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Show loading spinner with message
|
|
226
|
+
* Why: Provide visual feedback during long operations
|
|
227
|
+
*
|
|
228
|
+
* @param {string} message - Loading message
|
|
229
|
+
* @returns {Function} - Stop function to clear spinner
|
|
230
|
+
*/
|
|
231
|
+
export const showSpinner = (message) => {
|
|
232
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
233
|
+
let frameIndex = 0;
|
|
234
|
+
let isActive = true;
|
|
235
|
+
|
|
236
|
+
const interval = setInterval(() => {
|
|
237
|
+
if (!isActive) {
|
|
238
|
+
clearInterval(interval);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const frame = frames[frameIndex];
|
|
243
|
+
process.stdout.write(`\r${frame} ${message}`);
|
|
244
|
+
frameIndex = (frameIndex + 1) % frames.length;
|
|
245
|
+
}, 80);
|
|
246
|
+
|
|
247
|
+
// Return stop function
|
|
248
|
+
return () => {
|
|
249
|
+
isActive = false;
|
|
250
|
+
clearInterval(interval);
|
|
251
|
+
process.stdout.write('\r'); // Clear line
|
|
252
|
+
};
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Show success message with checkmark
|
|
257
|
+
* @param {string} message - Success message
|
|
258
|
+
*/
|
|
259
|
+
export const showSuccess = (message) => {
|
|
260
|
+
console.log(`✅ ${message}`);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Show error message with X mark
|
|
265
|
+
* @param {string} message - Error message
|
|
266
|
+
*/
|
|
267
|
+
export const showError = (message) => {
|
|
268
|
+
console.log(`❌ ${message}`);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Show warning message with warning sign
|
|
273
|
+
* @param {string} message - Warning message
|
|
274
|
+
*/
|
|
275
|
+
export const showWarning = (message) => {
|
|
276
|
+
console.log(`⚠️ ${message}`);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Show info message with info icon
|
|
281
|
+
* @param {string} message - Info message
|
|
282
|
+
*/
|
|
283
|
+
export const showInfo = (message) => {
|
|
284
|
+
console.log(`ℹ️ ${message}`);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Clear console screen
|
|
289
|
+
* Why: Clean slate for new UI sections
|
|
290
|
+
*/
|
|
291
|
+
export const clearScreen = () => {
|
|
292
|
+
console.clear();
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Wait for user to press Enter
|
|
297
|
+
* Why: Pause before continuing
|
|
298
|
+
*
|
|
299
|
+
* @param {string} message - Message to show (default: "Press Enter to continue")
|
|
300
|
+
* @returns {Promise<void>}
|
|
301
|
+
*/
|
|
302
|
+
export const waitForEnter = async (message = 'Press Enter to continue') => {
|
|
303
|
+
const rl = readline.createInterface({
|
|
304
|
+
input: process.stdin,
|
|
305
|
+
output: process.stdout
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
return new Promise((resolve) => {
|
|
309
|
+
rl.question(`\n${message}... `, () => {
|
|
310
|
+
rl.close();
|
|
311
|
+
resolve();
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
};
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: mcp-setup.js
|
|
3
|
+
* Purpose: Automated GitHub MCP setup for Claude CLI
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Auto-detect existing configuration
|
|
7
|
+
* - Read token from Claude Desktop config
|
|
8
|
+
* - Interactive token prompt if needed
|
|
9
|
+
* - Configure Claude CLI MCP
|
|
10
|
+
* - Set environment variables
|
|
11
|
+
* - Verify configuration
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { execSync } from 'child_process';
|
|
15
|
+
import fs from 'fs';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import os from 'os';
|
|
18
|
+
import logger from './logger.js';
|
|
19
|
+
import { promptConfirmation, promptEditField, showSuccess, showError, showInfo, showWarning } from './interactive-ui.js';
|
|
20
|
+
import { getClaudeCommand } from './claude-client.js';
|
|
21
|
+
import { approveGitHubMcpPermissions, executeMcpCommand } from './github-client.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if GitHub MCP is already configured in Claude CLI
|
|
25
|
+
* Why: Avoid duplicate configuration
|
|
26
|
+
*
|
|
27
|
+
* @returns {boolean} - True if configured
|
|
28
|
+
*/
|
|
29
|
+
export const isGitHubMCPConfigured = () => {
|
|
30
|
+
try {
|
|
31
|
+
const { command, args } = getClaudeCommand();
|
|
32
|
+
const fullCommand = `${command} ${args.join(' ')} mcp list`;
|
|
33
|
+
const mcpList = execSync(fullCommand, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
|
|
34
|
+
return mcpList.toLowerCase().includes('github');
|
|
35
|
+
} catch (error) {
|
|
36
|
+
logger.debug('mcp-setup - isGitHubMCPConfigured', 'Failed to check MCP list', { error: error.message });
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Find GitHub token in Claude Desktop config
|
|
43
|
+
* Why: Reuse existing token instead of asking user
|
|
44
|
+
*
|
|
45
|
+
* @returns {Object|null} - { token, configPath } or null if not found
|
|
46
|
+
*/
|
|
47
|
+
export const findGitHubTokenInDesktopConfig = () => {
|
|
48
|
+
const possibleConfigPaths = [
|
|
49
|
+
// Linux/macOS standard path
|
|
50
|
+
path.join(os.homedir(), '.config', 'Claude', 'claude_desktop_config.json'),
|
|
51
|
+
// Windows native path
|
|
52
|
+
path.join(os.homedir(), 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json'),
|
|
53
|
+
// WSL accessing Windows path - try multiple user names
|
|
54
|
+
'/mnt/c/Users/' + os.userInfo().username + '/AppData/Roaming/Claude/claude_desktop_config.json',
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
// Add additional Windows paths by checking if we're in WSL
|
|
58
|
+
if (process.platform === 'linux' && fs.existsSync('/mnt/c')) {
|
|
59
|
+
// We're in WSL, try to find all Windows user directories
|
|
60
|
+
try {
|
|
61
|
+
const usersDir = '/mnt/c/Users';
|
|
62
|
+
if (fs.existsSync(usersDir)) {
|
|
63
|
+
const users = fs.readdirSync(usersDir);
|
|
64
|
+
for (const user of users) {
|
|
65
|
+
const configPath = `/mnt/c/Users/${user}/AppData/Roaming/Claude/claude_desktop_config.json`;
|
|
66
|
+
if (!possibleConfigPaths.includes(configPath)) {
|
|
67
|
+
possibleConfigPaths.push(configPath);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
logger.debug('mcp-setup - findGitHubTokenInDesktopConfig', 'Failed to scan /mnt/c/Users', {
|
|
73
|
+
error: error.message
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const configPath of possibleConfigPaths) {
|
|
79
|
+
try {
|
|
80
|
+
if (fs.existsSync(configPath)) {
|
|
81
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
82
|
+
const token = config.mcpServers?.github?.env?.GITHUB_PERSONAL_ACCESS_TOKEN;
|
|
83
|
+
|
|
84
|
+
if (token) {
|
|
85
|
+
logger.debug('mcp-setup - findGitHubTokenInDesktopConfig', 'Found token', { configPath });
|
|
86
|
+
return { token, configPath };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
logger.debug('mcp-setup - findGitHubTokenInDesktopConfig', 'Failed to read config', {
|
|
91
|
+
configPath,
|
|
92
|
+
error: error.message
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return null;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Add GitHub MCP to Claude CLI
|
|
102
|
+
* Why: Configure MCP server for PR creation
|
|
103
|
+
*
|
|
104
|
+
* @param {boolean} replace - Replace existing configuration
|
|
105
|
+
* @returns {Promise<boolean>} - True if successful
|
|
106
|
+
*/
|
|
107
|
+
export const addGitHubMCP = async (replace = false) => {
|
|
108
|
+
try {
|
|
109
|
+
const { command, args } = getClaudeCommand();
|
|
110
|
+
|
|
111
|
+
// Remove existing if replacing
|
|
112
|
+
if (replace) {
|
|
113
|
+
try {
|
|
114
|
+
const removeCmd = `${command} ${args.join(' ')} mcp remove github`;
|
|
115
|
+
execSync(removeCmd, { stdio: 'ignore' });
|
|
116
|
+
logger.debug('mcp-setup - addGitHubMCP', 'Removed existing GitHub MCP');
|
|
117
|
+
} catch (error) {
|
|
118
|
+
// Ignore errors, may not have remove command
|
|
119
|
+
logger.debug('mcp-setup - addGitHubMCP', 'Failed to remove (may not exist)', { error: error.message });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Add GitHub MCP
|
|
124
|
+
// Use -- to separate claude options from npx arguments
|
|
125
|
+
const mcpCommand = `${command} ${args.join(' ')} mcp add github npx -- -y @modelcontextprotocol/server-github`;
|
|
126
|
+
logger.debug('mcp-setup - addGitHubMCP', 'Running MCP add command', { mcpCommand });
|
|
127
|
+
|
|
128
|
+
execSync(mcpCommand, { stdio: 'inherit' });
|
|
129
|
+
|
|
130
|
+
logger.debug('mcp-setup - addGitHubMCP', 'Successfully added GitHub MCP');
|
|
131
|
+
return true;
|
|
132
|
+
|
|
133
|
+
} catch (error) {
|
|
134
|
+
logger.error('mcp-setup - addGitHubMCP', 'Failed to add GitHub MCP', error);
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Set environment variable in shell RC files
|
|
141
|
+
* Why: Persist GitHub token across sessions
|
|
142
|
+
*
|
|
143
|
+
* @param {string} token - GitHub Personal Access Token
|
|
144
|
+
* @returns {boolean} - True if set in at least one file
|
|
145
|
+
*/
|
|
146
|
+
export const setEnvironmentVariable = (token) => {
|
|
147
|
+
const envVarLine = `export GITHUB_PERSONAL_ACCESS_TOKEN="${token}"`;
|
|
148
|
+
const shellRcFiles = [
|
|
149
|
+
path.join(os.homedir(), '.bashrc'),
|
|
150
|
+
path.join(os.homedir(), '.bash_profile'),
|
|
151
|
+
path.join(os.homedir(), '.zshrc')
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
let envVarSet = false;
|
|
155
|
+
|
|
156
|
+
for (const rcFile of shellRcFiles) {
|
|
157
|
+
try {
|
|
158
|
+
if (fs.existsSync(rcFile)) {
|
|
159
|
+
const content = fs.readFileSync(rcFile, 'utf8');
|
|
160
|
+
|
|
161
|
+
// Check if already present
|
|
162
|
+
if (content.includes('GITHUB_PERSONAL_ACCESS_TOKEN')) {
|
|
163
|
+
logger.debug('mcp-setup - setEnvironmentVariable', 'Already present', { rcFile });
|
|
164
|
+
showInfo(`Environment variable already in ${path.basename(rcFile)}`);
|
|
165
|
+
envVarSet = true;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Add to file
|
|
170
|
+
fs.appendFileSync(rcFile, `\n# GitHub MCP token for Claude CLI\n${envVarLine}\n`);
|
|
171
|
+
showSuccess(`Added environment variable to ${path.basename(rcFile)}`);
|
|
172
|
+
logger.debug('mcp-setup - setEnvironmentVariable', 'Added to file', { rcFile });
|
|
173
|
+
envVarSet = true;
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
logger.debug('mcp-setup - setEnvironmentVariable', 'Failed to update file', {
|
|
177
|
+
rcFile,
|
|
178
|
+
error: error.message
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!envVarSet) {
|
|
184
|
+
showWarning('Could not find shell RC file (.bashrc, .bash_profile, .zshrc)');
|
|
185
|
+
console.log('');
|
|
186
|
+
console.log('Please add this line to your shell configuration manually:');
|
|
187
|
+
console.log(` ${envVarLine}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Set for current session
|
|
191
|
+
process.env.GITHUB_PERSONAL_ACCESS_TOKEN = token;
|
|
192
|
+
|
|
193
|
+
return envVarSet;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Setup GitHub MCP for Claude CLI
|
|
198
|
+
* Why: Automate MCP configuration to enable create-pr functionality
|
|
199
|
+
*
|
|
200
|
+
* Interactive setup process:
|
|
201
|
+
* 1. Check if GitHub MCP already configured
|
|
202
|
+
* 2. Try to read GitHub token from Claude Desktop config
|
|
203
|
+
* 3. If not found, prompt user for token
|
|
204
|
+
* 4. Run: claude mcp add github npx -y @modelcontextprotocol/server-github
|
|
205
|
+
* 5. Configure environment variable for token
|
|
206
|
+
* 6. Verify configuration
|
|
207
|
+
*
|
|
208
|
+
* @returns {Promise<void>}
|
|
209
|
+
*/
|
|
210
|
+
export const setupGitHubMCP = async () => {
|
|
211
|
+
try {
|
|
212
|
+
console.log('');
|
|
213
|
+
showInfo('GitHub MCP Setup for Claude CLI');
|
|
214
|
+
console.log('');
|
|
215
|
+
|
|
216
|
+
// Step 1: Check if already configured
|
|
217
|
+
console.log('🔍 Checking current MCP configuration...');
|
|
218
|
+
|
|
219
|
+
let mcpList = '';
|
|
220
|
+
try {
|
|
221
|
+
const { command, args } = getClaudeCommand();
|
|
222
|
+
const fullCommand = `${command} ${args.join(' ')} mcp list`;
|
|
223
|
+
mcpList = execSync(fullCommand, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
|
|
224
|
+
} catch (error) {
|
|
225
|
+
showError('Failed to run "claude mcp list". Is Claude CLI installed?');
|
|
226
|
+
console.error('Make sure Claude CLI is installed: npm install -g @anthropic-ai/claude-cli');
|
|
227
|
+
console.error('Error:', error.message);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const hasGitHub = isGitHubMCPConfigured();
|
|
232
|
+
if (hasGitHub) {
|
|
233
|
+
showSuccess('GitHub MCP is already configured!');
|
|
234
|
+
console.log('');
|
|
235
|
+
console.log(mcpList);
|
|
236
|
+
|
|
237
|
+
const reconfigure = await promptConfirmation('Do you want to reconfigure it?', false);
|
|
238
|
+
if (!reconfigure) {
|
|
239
|
+
showInfo('Setup cancelled. Your existing configuration is unchanged.');
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Step 2: Try to read token from Claude Desktop config
|
|
245
|
+
console.log('');
|
|
246
|
+
console.log('🔑 Looking for GitHub token...');
|
|
247
|
+
|
|
248
|
+
let githubToken = null;
|
|
249
|
+
const tokenInfo = findGitHubTokenInDesktopConfig();
|
|
250
|
+
|
|
251
|
+
if (tokenInfo) {
|
|
252
|
+
githubToken = tokenInfo.token;
|
|
253
|
+
showSuccess('Found GitHub token in Claude Desktop config');
|
|
254
|
+
console.log(` Location: ${tokenInfo.configPath}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Step 3: If not found, prompt user
|
|
258
|
+
if (!githubToken) {
|
|
259
|
+
showWarning('No GitHub token found in Claude Desktop config');
|
|
260
|
+
console.log('');
|
|
261
|
+
console.log('You need a GitHub Personal Access Token with these permissions:');
|
|
262
|
+
console.log(' - repo (Full control of private repositories)');
|
|
263
|
+
console.log(' - read:org (Read org and team membership)');
|
|
264
|
+
console.log('');
|
|
265
|
+
console.log('Create one at: https://github.com/settings/tokens/new');
|
|
266
|
+
console.log('');
|
|
267
|
+
|
|
268
|
+
githubToken = await promptEditField('GitHub Personal Access Token', '');
|
|
269
|
+
|
|
270
|
+
if (!githubToken || githubToken.trim() === '') {
|
|
271
|
+
showError('GitHub token is required. Setup cancelled.');
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Step 4: Add GitHub MCP to Claude CLI
|
|
277
|
+
console.log('');
|
|
278
|
+
console.log('⚙️ Configuring GitHub MCP for Claude CLI...');
|
|
279
|
+
|
|
280
|
+
const added = await addGitHubMCP(hasGitHub);
|
|
281
|
+
if (!added) {
|
|
282
|
+
showError('Failed to add GitHub MCP');
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
showSuccess('GitHub MCP added to Claude CLI');
|
|
287
|
+
|
|
288
|
+
// Step 5: Configure environment variable
|
|
289
|
+
console.log('');
|
|
290
|
+
console.log('🔧 Setting up environment variable...');
|
|
291
|
+
|
|
292
|
+
setEnvironmentVariable(githubToken);
|
|
293
|
+
|
|
294
|
+
// Step 6: Approve MCP permissions
|
|
295
|
+
console.log('');
|
|
296
|
+
console.log('🔐 Approving MCP permissions...');
|
|
297
|
+
|
|
298
|
+
const permResult = await approveGitHubMcpPermissions();
|
|
299
|
+
if (permResult.success) {
|
|
300
|
+
showSuccess('MCP permissions approved');
|
|
301
|
+
} else {
|
|
302
|
+
showWarning('Some permissions may need manual approval');
|
|
303
|
+
console.log(' Run: claude mcp approve github --all');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Step 7: Verify configuration
|
|
307
|
+
console.log('');
|
|
308
|
+
console.log('✅ Verifying configuration...');
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const { command, args } = getClaudeCommand();
|
|
312
|
+
const fullCommand = `${command} ${args.join(' ')} mcp list`;
|
|
313
|
+
const verifyList = execSync(fullCommand, { encoding: 'utf8' });
|
|
314
|
+
if (verifyList.toLowerCase().includes('github')) {
|
|
315
|
+
showSuccess('GitHub MCP is configured and ready!');
|
|
316
|
+
console.log('');
|
|
317
|
+
console.log(verifyList);
|
|
318
|
+
} else {
|
|
319
|
+
showWarning('Configuration completed but GitHub MCP not showing in list');
|
|
320
|
+
console.log('You may need to restart your terminal');
|
|
321
|
+
}
|
|
322
|
+
} catch (error) {
|
|
323
|
+
showWarning('Could not verify configuration');
|
|
324
|
+
logger.debug('mcp-setup - setupGitHubMCP', 'Verification failed', { error: error.message });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Final instructions
|
|
328
|
+
console.log('');
|
|
329
|
+
showSuccess('Setup complete!');
|
|
330
|
+
console.log('');
|
|
331
|
+
console.log('Next steps:');
|
|
332
|
+
console.log(' 1. Restart your terminal (or run: source ~/.bashrc)');
|
|
333
|
+
console.log(' 2. Verify with: claude mcp list');
|
|
334
|
+
console.log(' 3. Try creating a PR: claude-hooks create-pr develop');
|
|
335
|
+
console.log('');
|
|
336
|
+
|
|
337
|
+
} catch (error) {
|
|
338
|
+
showError('Error during MCP setup: ' + error.message);
|
|
339
|
+
console.error(error);
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
};
|