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
package/bin/claude-hooks
CHANGED
|
@@ -1,2324 +1,111 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { execSync, spawn } from 'child_process';
|
|
4
|
-
import fs from 'fs';
|
|
5
|
-
import path from 'path';
|
|
6
|
-
import os from 'os';
|
|
7
|
-
import readline from 'readline';
|
|
8
|
-
import https from 'https';
|
|
9
|
-
import { fileURLToPath } from 'url';
|
|
10
|
-
import { dirname } from 'path';
|
|
11
|
-
import { executeClaude, executeClaudeWithRetry, extractJSON, analyzeCode } from '../lib/utils/claude-client.js';
|
|
12
|
-
import { loadPrompt } from '../lib/utils/prompt-builder.js';
|
|
13
|
-
import { listPresets } from '../lib/utils/preset-loader.js';
|
|
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 { displayStatistics as showTelemetryStats, clearTelemetry as clearTelemetryData } from '../lib/utils/telemetry.js';
|
|
20
|
-
import logger from '../lib/utils/logger.js';
|
|
21
|
-
|
|
22
|
-
// Why: ES6 modules don't have __dirname, need to recreate it
|
|
23
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
24
|
-
const __dirname = dirname(__filename);
|
|
25
|
-
|
|
26
|
-
// Helper to read package.json
|
|
27
|
-
// Why: ES6 modules can't use require() for JSON files
|
|
28
|
-
const getPackageJson = () => {
|
|
29
|
-
const packagePath = path.join(__dirname, '..', 'package.json');
|
|
30
|
-
return JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
// Function to get the latest version from NPM
|
|
34
|
-
function getLatestVersion(packageName) {
|
|
35
|
-
return new Promise((resolve, reject) => {
|
|
36
|
-
// Use the main NPM API, not /latest
|
|
37
|
-
https.get(`https://registry.npmjs.org/${packageName}`, (res) => {
|
|
38
|
-
let data = '';
|
|
39
|
-
res.on('data', chunk => data += chunk);
|
|
40
|
-
res.on('end', () => {
|
|
41
|
-
try {
|
|
42
|
-
const json = JSON.parse(data);
|
|
43
|
-
// Get the version from the 'latest' tag
|
|
44
|
-
if (json['dist-tags'] && json['dist-tags'].latest) {
|
|
45
|
-
resolve(json['dist-tags'].latest);
|
|
46
|
-
} else {
|
|
47
|
-
reject(new Error('Could not get the version'));
|
|
48
|
-
}
|
|
49
|
-
} catch (e) {
|
|
50
|
-
// If it fails, try with npm view
|
|
51
|
-
try {
|
|
52
|
-
const version = execSync(`npm view ${packageName} version`, { encoding: 'utf8' }).trim();
|
|
53
|
-
resolve(version);
|
|
54
|
-
} catch (npmError) {
|
|
55
|
-
reject(e);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
}).on('error', reject);
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Function to check version (used by hooks)
|
|
64
|
-
async function checkVersionAndPromptUpdate() {
|
|
65
|
-
try {
|
|
66
|
-
const currentVersion = getPackageJson().version;
|
|
67
|
-
const latestVersion = await getLatestVersion('claude-git-hooks');
|
|
68
|
-
|
|
69
|
-
if (currentVersion === latestVersion) {
|
|
70
|
-
return true; // Already updated
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
console.log('');
|
|
74
|
-
warning(`New version available: ${latestVersion} (current: ${currentVersion})`);
|
|
75
|
-
|
|
76
|
-
// Interactive prompt compatible with all consoles
|
|
77
|
-
const rl = readline.createInterface({
|
|
78
|
-
input: process.stdin,
|
|
79
|
-
output: process.stdout
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
return new Promise((resolve) => {
|
|
83
|
-
rl.question('Do you want to update now? (y/n): ', (answer) => {
|
|
84
|
-
rl.close();
|
|
85
|
-
|
|
86
|
-
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
|
87
|
-
info('Updating claude-git-hooks...');
|
|
88
|
-
try {
|
|
89
|
-
execSync('npm install -g claude-git-hooks@latest', { stdio: 'inherit' });
|
|
90
|
-
success('Update completed. Please run your command again.');
|
|
91
|
-
process.exit(0); // Exit so user restarts the process
|
|
92
|
-
} catch (e) {
|
|
93
|
-
error('Error updating: ' + e.message);
|
|
94
|
-
resolve(false);
|
|
95
|
-
}
|
|
96
|
-
} else {
|
|
97
|
-
info('Update postponed. You can update later with: claude-hooks update');
|
|
98
|
-
resolve(true); // Continue without updating
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
} catch (e) {
|
|
103
|
-
// If there's an error checking version, continue without blocking
|
|
104
|
-
return true;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Colors for output
|
|
109
|
-
const colors = {
|
|
110
|
-
reset: '\x1b[0m',
|
|
111
|
-
red: '\x1b[31m',
|
|
112
|
-
green: '\x1b[32m',
|
|
113
|
-
yellow: '\x1b[33m',
|
|
114
|
-
blue: '\x1b[34m'
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
function log(message, color = 'reset') {
|
|
118
|
-
console.log(`${colors[color]}${message}${colors.reset}`);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function error(message) {
|
|
122
|
-
console.error(`${colors.red}❌ ${message}${colors.reset}`);
|
|
123
|
-
process.exit(1);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function success(message) {
|
|
127
|
-
log(`✅ ${message}`, 'green');
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function info(message) {
|
|
131
|
-
log(`ℹ️ ${message}`, 'blue');
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function warning(message) {
|
|
135
|
-
log(`⚠️ ${message}`, 'yellow');
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Entertainment system
|
|
139
|
-
class Entertainment {
|
|
140
|
-
static jokes = [
|
|
141
|
-
"Why do programmers prefer dark mode? Because light attracts bugs!",
|
|
142
|
-
"A QA engineer walks into a bar. Orders 1 beer. Orders 0 beers. Orders -1 beers.",
|
|
143
|
-
"What's a pirate's favorite programming language? R!",
|
|
144
|
-
"There are 10 types of people: those who understand binary and those who don't.",
|
|
145
|
-
"Why do programmers confuse Halloween with Christmas? Because Oct 31 = Dec 25",
|
|
146
|
-
"What does one bit say to another? See you on the bus!",
|
|
147
|
-
"Why don't Java and C++ get along? Because they have different views on pointers.",
|
|
148
|
-
"My code doesn't have bugs, just undocumented features."
|
|
149
|
-
];
|
|
150
|
-
|
|
151
|
-
static async getJoke() {
|
|
152
|
-
return new Promise((resolve) => {
|
|
153
|
-
// Try to get joke from API
|
|
154
|
-
const req = https.get('https://icanhazdadjoke.com/', {
|
|
155
|
-
headers: { 'Accept': 'text/plain' },
|
|
156
|
-
timeout: 3000
|
|
157
|
-
}, (res) => {
|
|
158
|
-
let data = '';
|
|
159
|
-
res.on('data', chunk => data += chunk);
|
|
160
|
-
res.on('end', () => resolve(data.trim()));
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
req.on('error', () => {
|
|
164
|
-
// If it fails, use local joke
|
|
165
|
-
const randomJoke = this.jokes[Math.floor(Math.random() * this.jokes.length)];
|
|
166
|
-
resolve(randomJoke);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
req.on('timeout', () => {
|
|
170
|
-
req.abort();
|
|
171
|
-
const randomJoke = this.jokes[Math.floor(Math.random() * this.jokes.length)];
|
|
172
|
-
resolve(randomJoke);
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
static async showSpinner(promise, message = 'Processing') {
|
|
178
|
-
const spinners = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
179
|
-
let spinnerIndex = 0;
|
|
180
|
-
let jokeCountdown = 10;
|
|
181
|
-
let currentJoke = this.jokes[Math.floor(Math.random() * this.jokes.length)];
|
|
182
|
-
let isFinished = false;
|
|
183
|
-
let isFirstRender = true;
|
|
184
|
-
|
|
185
|
-
// Get first joke from API without blocking
|
|
186
|
-
this.getJoke().then(joke => {
|
|
187
|
-
if (!isFinished) currentJoke = joke;
|
|
188
|
-
}).catch(() => { }); // If it fails, keep the local one
|
|
189
|
-
|
|
190
|
-
// Hide cursor
|
|
191
|
-
process.stdout.write('\x1B[?25l');
|
|
192
|
-
|
|
193
|
-
// Reserve space for the 3 lines
|
|
194
|
-
process.stdout.write('\n\n\n');
|
|
195
|
-
|
|
196
|
-
const interval = setInterval(() => {
|
|
197
|
-
if (isFinished) {
|
|
198
|
-
clearInterval(interval);
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
spinnerIndex++;
|
|
203
|
-
|
|
204
|
-
// Update countdown every second (10 iterations of 100ms)
|
|
205
|
-
if (spinnerIndex % 10 === 0) {
|
|
206
|
-
jokeCountdown--;
|
|
207
|
-
|
|
208
|
-
// Refresh joke every 10 seconds
|
|
209
|
-
if (jokeCountdown <= 0) {
|
|
210
|
-
this.getJoke().then(joke => {
|
|
211
|
-
if (!isFinished) currentJoke = joke;
|
|
212
|
-
}).catch(() => {
|
|
213
|
-
if (!isFinished) {
|
|
214
|
-
currentJoke = this.jokes[Math.floor(Math.random() * this.jokes.length)];
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
jokeCountdown = 10;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Always go back exactly 3 lines up
|
|
222
|
-
process.stdout.write('\x1B[3A');
|
|
223
|
-
|
|
224
|
-
// Render the 3 lines from the beginning
|
|
225
|
-
const spinner = spinners[spinnerIndex % spinners.length];
|
|
226
|
-
|
|
227
|
-
// Line 1: Spinner
|
|
228
|
-
process.stdout.write('\r\x1B[2K' + `${colors.yellow}${spinner} ${message}${colors.reset}\n`);
|
|
229
|
-
|
|
230
|
-
// Line 2: Joke
|
|
231
|
-
process.stdout.write('\r\x1B[2K' + `${colors.green}🎭 ${currentJoke}${colors.reset}\n`);
|
|
232
|
-
|
|
233
|
-
// Line 3: Countdown
|
|
234
|
-
process.stdout.write('\r\x1B[2K' + `${colors.yellow}⏱️ Next joke in: ${jokeCountdown}s${colors.reset}\n`);
|
|
235
|
-
}, 100);
|
|
236
|
-
|
|
237
|
-
try {
|
|
238
|
-
const result = await promise;
|
|
239
|
-
isFinished = true;
|
|
240
|
-
clearInterval(interval);
|
|
241
|
-
|
|
242
|
-
// Clean exactly 3 lines completely
|
|
243
|
-
process.stdout.write('\x1B[3A'); // Go up 3 lines
|
|
244
|
-
process.stdout.write('\r\x1B[2K'); // Clean line 1
|
|
245
|
-
process.stdout.write('\n\r\x1B[2K'); // Go down and clean line 2
|
|
246
|
-
process.stdout.write('\n\r\x1B[2K'); // Go down and clean line 3
|
|
247
|
-
process.stdout.write('\x1B[2A'); // Go up 2 lines to end up on the first
|
|
248
|
-
process.stdout.write('\r'); // Go to beginning of line
|
|
249
|
-
|
|
250
|
-
// Show cursor
|
|
251
|
-
process.stdout.write('\x1B[?25h');
|
|
252
|
-
|
|
253
|
-
return result;
|
|
254
|
-
} catch (error) {
|
|
255
|
-
isFinished = true;
|
|
256
|
-
clearInterval(interval);
|
|
257
|
-
|
|
258
|
-
// Clean exactly 3 lines completely
|
|
259
|
-
process.stdout.write('\x1B[3A'); // Go up 3 lines
|
|
260
|
-
process.stdout.write('\r\x1B[2K'); // Clean line 1
|
|
261
|
-
process.stdout.write('\n\r\x1B[2K'); // Go down and clean line 2
|
|
262
|
-
process.stdout.write('\n\r\x1B[2K'); // Go down and clean line 3
|
|
263
|
-
process.stdout.write('\x1B[2A'); // Go up 2 lines to end up on the first
|
|
264
|
-
process.stdout.write('\r'); // Go to beginning of line
|
|
265
|
-
|
|
266
|
-
// Show cursor
|
|
267
|
-
process.stdout.write('\x1B[?25h');
|
|
268
|
-
|
|
269
|
-
throw error;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Check if we are in a git repository (including worktrees created in PowerShell)
|
|
275
|
-
function checkGitRepo() {
|
|
276
|
-
try {
|
|
277
|
-
execSync('git rev-parse --git-dir', { stdio: 'ignore' });
|
|
278
|
-
return true;
|
|
279
|
-
} catch (e) {
|
|
280
|
-
// Try to detect worktree created in PowerShell
|
|
281
|
-
try {
|
|
282
|
-
if (fs.existsSync('.git')) {
|
|
283
|
-
const gitContent = fs.readFileSync('.git', 'utf8').trim();
|
|
284
|
-
// Check if it's a worktree pointer (gitdir: ...)
|
|
285
|
-
if (gitContent.startsWith('gitdir:')) {
|
|
286
|
-
let gitdir = gitContent.substring(8).trim();
|
|
287
|
-
// Convert Windows path to WSL if needed (C:\ -> /mnt/c/)
|
|
288
|
-
if (/^[A-Za-z]:/.test(gitdir)) {
|
|
289
|
-
gitdir = gitdir.replace(/^([A-Za-z]):/, (_, drive) => `/mnt/${drive.toLowerCase()}`);
|
|
290
|
-
gitdir = gitdir.replace(/\\/g, '/');
|
|
291
|
-
}
|
|
292
|
-
// Verify the gitdir exists
|
|
293
|
-
if (fs.existsSync(gitdir)) {
|
|
294
|
-
return true;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
} catch (worktreeError) {
|
|
299
|
-
// Ignore worktree detection errors
|
|
300
|
-
}
|
|
301
|
-
return false;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Get the templates path
|
|
306
|
-
function getTemplatesPath() {
|
|
307
|
-
return path.join(__dirname, '..', 'templates');
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Install command
|
|
311
|
-
async function install(args) {
|
|
312
|
-
if (!checkGitRepo()) {
|
|
313
|
-
error('You are not in a Git repository. Please run this command from the root of a repository.');
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const isForce = args.includes('--force');
|
|
317
|
-
const skipAuth = args.includes('--skip-auth');
|
|
318
|
-
|
|
319
|
-
// Check for updates (unless --skip-auth flag)
|
|
320
|
-
if (!skipAuth && !isForce) {
|
|
321
|
-
await checkVersionAndPromptUpdate();
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (isForce) {
|
|
325
|
-
info('Installing Claude Git Hooks (force mode)...');
|
|
326
|
-
} else {
|
|
327
|
-
info('Installing Claude Git Hooks...');
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// v2.0.0+: No sudo needed (pure Node.js, no system packages required)
|
|
331
|
-
// Check dependencies
|
|
332
|
-
await checkAndInstallDependencies(null, skipAuth);
|
|
333
|
-
|
|
334
|
-
const templatesPath = getTemplatesPath();
|
|
335
|
-
const hooksPath = '.git/hooks';
|
|
336
|
-
|
|
337
|
-
// Create hooks directory if it doesn't exist
|
|
338
|
-
if (!fs.existsSync(hooksPath)) {
|
|
339
|
-
fs.mkdirSync(hooksPath, { recursive: true });
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Helper function to copy file with LF line endings
|
|
343
|
-
// Why: Bash scripts must have LF (Unix) line endings, not CRLF (Windows)
|
|
344
|
-
const copyWithLF = (sourcePath, destPath) => {
|
|
345
|
-
let content = fs.readFileSync(sourcePath, 'utf8');
|
|
346
|
-
// Convert CRLF to LF
|
|
347
|
-
content = content.replace(/\r\n/g, '\n');
|
|
348
|
-
fs.writeFileSync(destPath, content, 'utf8');
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
// Hooks to install
|
|
352
|
-
const hooks = ['pre-commit', 'prepare-commit-msg'];
|
|
353
|
-
|
|
354
|
-
hooks.forEach(hook => {
|
|
355
|
-
const sourcePath = path.join(templatesPath, hook);
|
|
356
|
-
const destPath = path.join(hooksPath, hook);
|
|
357
|
-
|
|
358
|
-
// Make backup if it exists
|
|
359
|
-
if (fs.existsSync(destPath)) {
|
|
360
|
-
const backupPath = `${destPath}.backup.${Date.now()}`;
|
|
361
|
-
fs.copyFileSync(destPath, backupPath);
|
|
362
|
-
info(`Backup created: ${backupPath}`);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Copy hook with LF line endings (critical for bash)
|
|
366
|
-
copyWithLF(sourcePath, destPath);
|
|
367
|
-
fs.chmodSync(destPath, '755');
|
|
368
|
-
success(`${hook} installed`);
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
// Copy version verification script with LF line endings
|
|
372
|
-
const checkVersionSource = path.join(templatesPath, 'check-version.sh');
|
|
373
|
-
const checkVersionDest = path.join(hooksPath, 'check-version.sh');
|
|
374
|
-
|
|
375
|
-
if (fs.existsSync(checkVersionSource)) {
|
|
376
|
-
copyWithLF(checkVersionSource, checkVersionDest);
|
|
377
|
-
fs.chmodSync(checkVersionDest, '755');
|
|
378
|
-
success('Version verification script installed');
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Create .claude directory if it doesn't exist
|
|
382
|
-
const claudeDir = '.claude';
|
|
383
|
-
if (!fs.existsSync(claudeDir)) {
|
|
384
|
-
fs.mkdirSync(claudeDir, { recursive: true });
|
|
385
|
-
success('.claude directory created');
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Remove old SONAR template files if they exist (migration from v2.6.x to v2.7.0+)
|
|
389
|
-
const oldSonarFiles = [
|
|
390
|
-
'CLAUDE_PRE_COMMIT_SONAR.md',
|
|
391
|
-
'CLAUDE_ANALYSIS_PROMPT_SONAR.md'
|
|
392
|
-
];
|
|
393
|
-
|
|
394
|
-
oldSonarFiles.forEach(oldFile => {
|
|
395
|
-
const oldPath = path.join(claudeDir, oldFile);
|
|
396
|
-
if (fs.existsSync(oldPath)) {
|
|
397
|
-
fs.unlinkSync(oldPath);
|
|
398
|
-
info(`Removed old template: ${oldFile}`);
|
|
399
|
-
}
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
// Create .claude/prompts directory for markdown templates
|
|
403
|
-
const promptsDir = path.join(claudeDir, 'prompts');
|
|
404
|
-
if (!fs.existsSync(promptsDir)) {
|
|
405
|
-
fs.mkdirSync(promptsDir, { recursive: true });
|
|
406
|
-
success('.claude/prompts directory created');
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Copy template files (.md and .json) to appropriate locations
|
|
410
|
-
const templateFiles = fs.readdirSync(templatesPath)
|
|
411
|
-
.filter(file => {
|
|
412
|
-
const filePath = path.join(templatesPath, file);
|
|
413
|
-
// Exclude example.json files and only include .md and .json files
|
|
414
|
-
return fs.statSync(filePath).isFile() &&
|
|
415
|
-
(file.endsWith('.md') || file.endsWith('.json')) &&
|
|
416
|
-
!file.includes('example.json');
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
templateFiles.forEach(file => {
|
|
420
|
-
const sourcePath = path.join(templatesPath, file);
|
|
421
|
-
let destPath;
|
|
422
|
-
let destLocation;
|
|
423
|
-
|
|
424
|
-
// .md files go to .claude/prompts/, .json files go to .claude/
|
|
425
|
-
if (file.endsWith('.md')) {
|
|
426
|
-
destPath = path.join(promptsDir, file);
|
|
427
|
-
destLocation = '.claude/prompts/';
|
|
428
|
-
} else {
|
|
429
|
-
destPath = path.join(claudeDir, file);
|
|
430
|
-
destLocation = '.claude/';
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// In force mode or if it doesn't exist, copy the file
|
|
434
|
-
if (isForce || !fs.existsSync(destPath)) {
|
|
435
|
-
if (fs.existsSync(sourcePath)) {
|
|
436
|
-
fs.copyFileSync(sourcePath, destPath);
|
|
437
|
-
success(`${file} installed in ${destLocation}`);
|
|
438
|
-
}
|
|
439
|
-
} else {
|
|
440
|
-
info(`${file} already exists (skipped)`);
|
|
441
|
-
}
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
// Clean up old .md files from .claude/ root (v2.8.0 migration)
|
|
445
|
-
// .md files should now be in .claude/prompts/, not .claude/
|
|
446
|
-
const oldMdFiles = fs.readdirSync(claudeDir)
|
|
447
|
-
.filter(file => {
|
|
448
|
-
const filePath = path.join(claudeDir, file);
|
|
449
|
-
return fs.statSync(filePath).isFile() && file.endsWith('.md');
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
if (oldMdFiles.length > 0) {
|
|
453
|
-
oldMdFiles.forEach(file => {
|
|
454
|
-
const oldPath = path.join(claudeDir, file);
|
|
455
|
-
fs.unlinkSync(oldPath);
|
|
456
|
-
info(`Removed old template from .claude/: ${file} (now in prompts/)`);
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Copy presets directory structure
|
|
461
|
-
const presetsSourcePath = path.join(templatesPath, 'presets');
|
|
462
|
-
const presetsDestPath = path.join(claudeDir, 'presets');
|
|
463
|
-
|
|
464
|
-
if (fs.existsSync(presetsSourcePath)) {
|
|
465
|
-
// Create presets directory in .claude
|
|
466
|
-
if (!fs.existsSync(presetsDestPath)) {
|
|
467
|
-
fs.mkdirSync(presetsDestPath, { recursive: true });
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Copy each preset directory
|
|
471
|
-
const presetDirs = fs.readdirSync(presetsSourcePath)
|
|
472
|
-
.filter(item => fs.statSync(path.join(presetsSourcePath, item)).isDirectory());
|
|
473
|
-
|
|
474
|
-
presetDirs.forEach(presetName => {
|
|
475
|
-
const presetSource = path.join(presetsSourcePath, presetName);
|
|
476
|
-
const presetDest = path.join(presetsDestPath, presetName);
|
|
477
|
-
|
|
478
|
-
// Create preset directory
|
|
479
|
-
if (!fs.existsSync(presetDest)) {
|
|
480
|
-
fs.mkdirSync(presetDest, { recursive: true });
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Copy all files in preset directory
|
|
484
|
-
const presetFiles = fs.readdirSync(presetSource);
|
|
485
|
-
presetFiles.forEach(file => {
|
|
486
|
-
const sourceFile = path.join(presetSource, file);
|
|
487
|
-
const destFile = path.join(presetDest, file);
|
|
488
|
-
|
|
489
|
-
if (fs.statSync(sourceFile).isFile()) {
|
|
490
|
-
if (isForce || !fs.existsSync(destFile)) {
|
|
491
|
-
fs.copyFileSync(sourceFile, destFile);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
});
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
success(`${presetDirs.length} presets installed in .claude/presets/`);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Special handling for config.json (v2.8.0+): backup old, create new simplified
|
|
501
|
-
const configPath = path.join(claudeDir, 'config.json');
|
|
502
|
-
const configOldDir = path.join(claudeDir, 'config_old');
|
|
503
|
-
const configExampleDir = path.join(claudeDir, 'config_example');
|
|
504
|
-
|
|
505
|
-
// Create config_old directory if needed
|
|
506
|
-
if (!fs.existsSync(configOldDir)) {
|
|
507
|
-
fs.mkdirSync(configOldDir, { recursive: true });
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Create config_example directory
|
|
511
|
-
if (!fs.existsSync(configExampleDir)) {
|
|
512
|
-
fs.mkdirSync(configExampleDir, { recursive: true });
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// Copy example configs to config_example/ directly from templates/
|
|
516
|
-
const exampleConfigs = ['config.example.json', 'config.advanced.example.json'];
|
|
517
|
-
exampleConfigs.forEach(exampleFile => {
|
|
518
|
-
const sourcePath = path.join(templatesPath, exampleFile);
|
|
519
|
-
const destPath = path.join(configExampleDir, exampleFile);
|
|
520
|
-
if (fs.existsSync(sourcePath)) {
|
|
521
|
-
fs.copyFileSync(sourcePath, destPath);
|
|
522
|
-
}
|
|
523
|
-
});
|
|
524
|
-
success('Example configs installed in .claude/config_example/');
|
|
525
|
-
|
|
526
|
-
// Backup existing config if it exists (legacy format migration)
|
|
527
|
-
let needsMigration = false;
|
|
528
|
-
if (fs.existsSync(configPath)) {
|
|
529
|
-
const backupPath = path.join(configOldDir, `config.json.${Date.now()}`);
|
|
530
|
-
fs.copyFileSync(configPath, backupPath);
|
|
531
|
-
info(`Existing config backed up: ${backupPath}`);
|
|
532
|
-
|
|
533
|
-
// Read old config to check if it's legacy format
|
|
534
|
-
const oldConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
535
|
-
if (!oldConfig.version || oldConfig.version !== '2.8.0') {
|
|
536
|
-
warning('Legacy config detected - will be replaced with v2.8.0 format');
|
|
537
|
-
needsMigration = true;
|
|
538
|
-
|
|
539
|
-
// Delete old config to force new format
|
|
540
|
-
fs.unlinkSync(configPath);
|
|
541
|
-
} else {
|
|
542
|
-
info('Config already in v2.8.0 format - keeping existing file');
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// Create new config.json from minimal example if it doesn't exist
|
|
547
|
-
if (!fs.existsSync(configPath)) {
|
|
548
|
-
// Read example and extract minimal config
|
|
549
|
-
const examplePath = path.join(configExampleDir, 'config.example.json');
|
|
550
|
-
const exampleContent = fs.readFileSync(examplePath, 'utf8');
|
|
551
|
-
const exampleJson = JSON.parse(exampleContent);
|
|
552
|
-
|
|
553
|
-
// Create minimal config: just version and preset
|
|
554
|
-
const minimalConfig = {
|
|
555
|
-
version: exampleJson.version,
|
|
556
|
-
preset: exampleJson.preset
|
|
557
|
-
};
|
|
558
|
-
|
|
559
|
-
fs.writeFileSync(configPath, JSON.stringify(minimalConfig, null, 4));
|
|
560
|
-
success('config.json created with minimal v2.8.0 format');
|
|
561
|
-
info('📝 Customize: .claude/config.json (see config_example/ for examples)');
|
|
562
|
-
|
|
563
|
-
// Auto-run migration if needed to preserve settings
|
|
564
|
-
if (needsMigration) {
|
|
565
|
-
info('🔄 Auto-migrating settings from backup...');
|
|
566
|
-
await autoMigrateConfig(configPath, path.join(configOldDir, fs.readdirSync(configOldDir).sort().pop()));
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// Create settings.local.json for sensitive data (gitignored)
|
|
571
|
-
const settingsLocalPath = path.join(claudeDir, 'settings.local.json');
|
|
572
|
-
if (!fs.existsSync(settingsLocalPath)) {
|
|
573
|
-
const settingsLocalContent = {
|
|
574
|
-
"_comment": "Local settings - DO NOT COMMIT. This file is gitignored.",
|
|
575
|
-
"githubToken": ""
|
|
576
|
-
};
|
|
577
|
-
fs.writeFileSync(settingsLocalPath, JSON.stringify(settingsLocalContent, null, 2));
|
|
578
|
-
info('settings.local.json created (add your GitHub token here)');
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// Configure Git
|
|
582
|
-
configureGit();
|
|
583
|
-
|
|
584
|
-
// Update .gitignore
|
|
585
|
-
updateGitignore();
|
|
586
|
-
|
|
587
|
-
success('Claude Git Hooks installed successfully! 🎉');
|
|
588
|
-
console.log('\nUsage:');
|
|
589
|
-
console.log(' git commit -m "auto" # Generate message automatically');
|
|
590
|
-
console.log(' git commit -m "message" # Analyze code before commit');
|
|
591
|
-
console.log(' git commit --no-verify # Skip analysis completely');
|
|
592
|
-
console.log('\n💡 Configuration (v2.8.0):');
|
|
593
|
-
console.log(' 📁 All templates installed in .claude/');
|
|
594
|
-
console.log(' 📝 Edit .claude/config.json (minimal by default)');
|
|
595
|
-
console.log(' 📂 Examples: .claude/config_example/');
|
|
596
|
-
console.log(' 📦 Backups: .claude/config_old/');
|
|
597
|
-
console.log(' 🎯 Presets: backend, frontend, fullstack, database, ai, default');
|
|
598
|
-
console.log(' 🚀 Parallel analysis enabled by default (hardcoded)');
|
|
599
|
-
console.log(' 🐛 Debug mode: claude-hooks --debug true');
|
|
600
|
-
console.log('\n🔗 GitHub PR Creation (v2.5.0+):');
|
|
601
|
-
console.log(' claude-hooks setup-github # Configure GitHub token');
|
|
602
|
-
console.log(' claude-hooks create-pr main # Create PR with auto-metadata');
|
|
603
|
-
console.log('\n📖 Minimal config.json (v2.8.0):');
|
|
604
|
-
console.log(' {');
|
|
605
|
-
console.log(' "version": "2.8.0",');
|
|
606
|
-
console.log(' "preset": "backend"');
|
|
607
|
-
console.log(' }');
|
|
608
|
-
console.log('\n📖 With GitHub customization:');
|
|
609
|
-
console.log(' {');
|
|
610
|
-
console.log(' "version": "2.8.0",');
|
|
611
|
-
console.log(' "preset": "backend",');
|
|
612
|
-
console.log(' "overrides": {');
|
|
613
|
-
console.log(' "github": { "pr": { "reviewers": ["your-username"] } }');
|
|
614
|
-
console.log(' }');
|
|
615
|
-
console.log(' }');
|
|
616
|
-
console.log('\n🔧 Advanced: see .claude/config_example/config.advanced.example.json');
|
|
617
|
-
console.log('\nFor more options: claude-hooks --help');
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// Check complete dependencies (like setup-wsl.sh)
|
|
621
|
-
async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false) {
|
|
622
|
-
info('Checking system dependencies...');
|
|
623
|
-
|
|
624
|
-
// Check Node.js
|
|
625
|
-
try {
|
|
626
|
-
const nodeVersion = execSync('node --version', { encoding: 'utf8' }).trim();
|
|
627
|
-
success(`Node.js ${nodeVersion}`);
|
|
628
|
-
} catch (e) {
|
|
629
|
-
error('Node.js is not installed. Install Node.js and try again.');
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
// Check npm
|
|
633
|
-
try {
|
|
634
|
-
const npmVersion = execSync('npm --version', { encoding: 'utf8' }).trim();
|
|
635
|
-
success(`npm ${npmVersion}`);
|
|
636
|
-
} catch (e) {
|
|
637
|
-
error('npm is not installed.');
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// v2.0.0+: jq and curl are no longer needed (pure Node.js implementation)
|
|
641
|
-
|
|
642
|
-
// Check Git
|
|
643
|
-
try {
|
|
644
|
-
const gitVersion = execSync('git --version', { encoding: 'utf8' }).trim();
|
|
645
|
-
success(`${gitVersion}`);
|
|
646
|
-
} catch (e) {
|
|
647
|
-
error('Git is not installed. Install Git and try again.');
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// v2.0.0+: Unix tools (sed, awk, grep, etc.) no longer needed (pure Node.js implementation)
|
|
651
|
-
|
|
652
|
-
// Check and install Claude CLI (skip if --skip-auth)
|
|
653
|
-
if (!skipAuth) {
|
|
654
|
-
await checkAndInstallClaude();
|
|
655
|
-
await checkClaudeAuth();
|
|
656
|
-
} else {
|
|
657
|
-
warning('Skipping Claude CLI verification and authentication (--skip-auth)');
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
// Clear password from memory
|
|
661
|
-
sudoPassword = null;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// Detect if running on Windows
|
|
665
|
-
// Why: Need to use 'wsl claude' instead of 'claude' on Windows
|
|
666
|
-
function isWindows() {
|
|
667
|
-
return os.platform() === 'win32' || process.env.OS === 'Windows_NT';
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// Get Claude command based on platform
|
|
671
|
-
// Why: On Windows, try native Claude first, then WSL as fallback
|
|
672
|
-
function getClaudeCommand() {
|
|
673
|
-
if (isWindows()) {
|
|
674
|
-
// Try native Windows Claude first
|
|
675
|
-
try {
|
|
676
|
-
execSync('claude --version', { stdio: 'ignore', timeout: 3000 });
|
|
677
|
-
return 'claude';
|
|
678
|
-
} catch (e) {
|
|
679
|
-
// Fallback to WSL
|
|
680
|
-
return 'wsl claude';
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
return 'claude';
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// Check if we need to install dependencies
|
|
687
|
-
async function checkIfInstallationNeeded() {
|
|
688
|
-
// v2.0.0+: Only check Claude CLI (jq and curl no longer needed)
|
|
689
|
-
const claudeCmd = getClaudeCommand();
|
|
690
|
-
|
|
691
|
-
try {
|
|
692
|
-
execSync(`${claudeCmd} --version`, { stdio: 'ignore' });
|
|
693
|
-
} catch (e) {
|
|
694
|
-
return true; // Needs Claude installation
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
return false;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
// Check Claude CLI availability
|
|
701
|
-
async function checkAndInstallClaude() {
|
|
702
|
-
const claudeCmd = getClaudeCommand();
|
|
703
|
-
const platform = isWindows() ? 'Windows (via WSL)' : os.platform();
|
|
704
|
-
|
|
705
|
-
try {
|
|
706
|
-
execSync(`${claudeCmd} --version`, { stdio: 'ignore' });
|
|
707
|
-
success(`Claude CLI detected (${platform})`);
|
|
708
|
-
} catch (e) {
|
|
709
|
-
error(`Claude CLI not detected on ${platform}`);
|
|
710
|
-
|
|
711
|
-
if (isWindows()) {
|
|
712
|
-
console.log('\n⚠️ On Windows, Claude CLI must be installed in WSL:');
|
|
713
|
-
console.log('1. Open WSL terminal (wsl or Ubuntu from Start Menu)');
|
|
714
|
-
console.log('2. Follow installation at: https://docs.anthropic.com/claude/docs/claude-cli');
|
|
715
|
-
console.log('3. Verify with: wsl claude --version');
|
|
716
|
-
} else {
|
|
717
|
-
console.log('\nClaude CLI installation: https://docs.anthropic.com/claude/docs/claude-cli');
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
console.log('\nAfter installation, run: claude-hooks install --force');
|
|
721
|
-
process.exit(1);
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
// Check Claude authentication with entertainment
|
|
726
|
-
async function checkClaudeAuth() {
|
|
727
|
-
info('Checking Claude authentication...');
|
|
728
|
-
|
|
729
|
-
// Get correct Claude command for platform
|
|
730
|
-
const claudeCmd = getClaudeCommand();
|
|
731
|
-
const cmdParts = claudeCmd.split(' ');
|
|
732
|
-
const command = cmdParts[0];
|
|
733
|
-
const args = [...cmdParts.slice(1), 'auth', 'status'];
|
|
734
|
-
|
|
735
|
-
// Use spawn to not block, but with stdio: 'ignore' like the original
|
|
736
|
-
const authPromise = new Promise((resolve, reject) => {
|
|
737
|
-
const child = spawn(command, args, {
|
|
738
|
-
stdio: 'ignore', // Igual que el original
|
|
739
|
-
detached: false,
|
|
740
|
-
windowsHide: true
|
|
741
|
-
});
|
|
742
|
-
|
|
743
|
-
// Manual timeout since spawn doesn't have native timeout
|
|
744
|
-
const timeout = setTimeout(() => {
|
|
745
|
-
child.kill();
|
|
746
|
-
reject(new Error('timeout'));
|
|
747
|
-
}, 120000); // 2 minutos
|
|
748
|
-
|
|
749
|
-
child.on('exit', (code) => {
|
|
750
|
-
clearTimeout(timeout);
|
|
751
|
-
if (code === 0) {
|
|
752
|
-
resolve('success');
|
|
753
|
-
} else {
|
|
754
|
-
reject(new Error('not_authenticated'));
|
|
755
|
-
}
|
|
756
|
-
});
|
|
757
|
-
|
|
758
|
-
child.on('error', (err) => {
|
|
759
|
-
clearTimeout(timeout);
|
|
760
|
-
reject(err);
|
|
761
|
-
});
|
|
762
|
-
});
|
|
763
|
-
|
|
764
|
-
try {
|
|
765
|
-
await Entertainment.showSpinner(authPromise, 'Checking Claude authentication');
|
|
766
|
-
success('Authenticated in Claude');
|
|
767
|
-
} catch (e) {
|
|
768
|
-
warning('You are not authenticated in Claude');
|
|
769
|
-
console.log('Run: claude auth login');
|
|
770
|
-
console.log('Then run this command again');
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
// Update .gitignore with Claude entries
|
|
775
|
-
function updateGitignore() {
|
|
776
|
-
info('Updating .gitignore...');
|
|
777
|
-
|
|
778
|
-
const gitignorePath = '.gitignore';
|
|
779
|
-
const claudeEntries = [
|
|
780
|
-
'# Claude Git Hooks (includes .claude/settings.local.json for tokens)',
|
|
781
|
-
'.claude/',
|
|
782
|
-
];
|
|
783
|
-
|
|
784
|
-
let gitignoreContent = '';
|
|
785
|
-
let fileExists = false;
|
|
786
|
-
|
|
787
|
-
// Read existing .gitignore if it exists
|
|
788
|
-
if (fs.existsSync(gitignorePath)) {
|
|
789
|
-
gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
|
790
|
-
fileExists = true;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
// Check which entries are missing
|
|
794
|
-
const missingEntries = [];
|
|
795
|
-
claudeEntries.forEach(entry => {
|
|
796
|
-
if (entry.startsWith('#')) {
|
|
797
|
-
// For comments, check if any Claude comment already exists
|
|
798
|
-
if (!gitignoreContent.includes('# Claude')) {
|
|
799
|
-
missingEntries.push(entry);
|
|
800
|
-
}
|
|
801
|
-
} else {
|
|
802
|
-
// For normal entries, check if they are already present
|
|
803
|
-
const regex = new RegExp(`^${entry.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm');
|
|
804
|
-
if (!regex.test(gitignoreContent)) {
|
|
805
|
-
missingEntries.push(entry);
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
});
|
|
809
|
-
|
|
810
|
-
// If there are missing entries, add them
|
|
811
|
-
if (missingEntries.length > 0) {
|
|
812
|
-
// Ensure there's a newline at the end if the file exists and is not empty
|
|
813
|
-
if (fileExists && gitignoreContent.length > 0 && !gitignoreContent.endsWith('\n')) {
|
|
814
|
-
gitignoreContent += '\n';
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
// If the file is not empty, add a blank line before
|
|
818
|
-
if (gitignoreContent.length > 0) {
|
|
819
|
-
gitignoreContent += '\n';
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
// Add the missing entries
|
|
823
|
-
gitignoreContent += missingEntries.join('\n') + '\n';
|
|
824
|
-
|
|
825
|
-
// Write the updated file
|
|
826
|
-
fs.writeFileSync(gitignorePath, gitignoreContent);
|
|
827
|
-
|
|
828
|
-
if (fileExists) {
|
|
829
|
-
success('.gitignore updated with Claude entries');
|
|
830
|
-
} else {
|
|
831
|
-
success('.gitignore created with Claude entries');
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
// Show what was added
|
|
835
|
-
missingEntries.forEach(entry => {
|
|
836
|
-
if (!entry.startsWith('#')) {
|
|
837
|
-
info(` + ${entry}`);
|
|
838
|
-
}
|
|
839
|
-
});
|
|
840
|
-
} else {
|
|
841
|
-
info('.gitignore already contains all necessary entries');
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
// Configure Git (line endings, etc.)
|
|
846
|
-
function configureGit() {
|
|
847
|
-
info('Configuring Git...');
|
|
848
|
-
|
|
849
|
-
try {
|
|
850
|
-
// Configure line endings based on platform
|
|
851
|
-
// Why: CRLF/LF handling differs between Windows and Unix
|
|
852
|
-
if (isWindows()) {
|
|
853
|
-
// On Windows: Keep CRLF in working directory, convert to LF in repo
|
|
854
|
-
execSync('git config core.autocrlf true', { stdio: 'ignore' });
|
|
855
|
-
success('Line endings configured for Windows (core.autocrlf = true)');
|
|
856
|
-
} else {
|
|
857
|
-
// On Unix: Keep LF everywhere, convert CRLF to LF on commit
|
|
858
|
-
execSync('git config core.autocrlf input', { stdio: 'ignore' });
|
|
859
|
-
success('Line endings configured for Unix (core.autocrlf = input)');
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
} catch (e) {
|
|
863
|
-
warning('Error configuring Git: ' + e.message);
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
// Uninstall command
|
|
868
|
-
function uninstall() {
|
|
869
|
-
if (!checkGitRepo()) {
|
|
870
|
-
error('You are not in a Git repository.');
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
info('Uninstalling Claude Git Hooks...');
|
|
874
|
-
|
|
875
|
-
const hooksPath = '.git/hooks';
|
|
876
|
-
const hooks = ['pre-commit', 'prepare-commit-msg'];
|
|
877
|
-
|
|
878
|
-
hooks.forEach(hook => {
|
|
879
|
-
const hookPath = path.join(hooksPath, hook);
|
|
880
|
-
if (fs.existsSync(hookPath)) {
|
|
881
|
-
fs.unlinkSync(hookPath);
|
|
882
|
-
success(`${hook} removed`);
|
|
883
|
-
}
|
|
884
|
-
});
|
|
885
|
-
|
|
886
|
-
success('Claude Git Hooks uninstalled');
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
// Enable command
|
|
890
|
-
function enable(hookName) {
|
|
891
|
-
if (!checkGitRepo()) {
|
|
892
|
-
error('You are not in a Git repository.');
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
const hooks = hookName ? [hookName] : ['pre-commit', 'prepare-commit-msg'];
|
|
896
|
-
|
|
897
|
-
hooks.forEach(hook => {
|
|
898
|
-
const disabledPath = `.git/hooks/${hook}.disabled`;
|
|
899
|
-
const enabledPath = `.git/hooks/${hook}`;
|
|
900
|
-
|
|
901
|
-
if (fs.existsSync(disabledPath)) {
|
|
902
|
-
fs.renameSync(disabledPath, enabledPath);
|
|
903
|
-
success(`${hook} enabled`);
|
|
904
|
-
} else if (fs.existsSync(enabledPath)) {
|
|
905
|
-
info(`${hook} is already enabled`);
|
|
906
|
-
} else {
|
|
907
|
-
warning(`${hook} not found`);
|
|
908
|
-
}
|
|
909
|
-
});
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
// Disable command
|
|
913
|
-
function disable(hookName) {
|
|
914
|
-
if (!checkGitRepo()) {
|
|
915
|
-
error('You are not in a Git repository.');
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
const hooks = hookName ? [hookName] : ['pre-commit', 'prepare-commit-msg'];
|
|
919
|
-
|
|
920
|
-
hooks.forEach(hook => {
|
|
921
|
-
const enabledPath = `.git/hooks/${hook}`;
|
|
922
|
-
const disabledPath = `.git/hooks/${hook}.disabled`;
|
|
923
|
-
|
|
924
|
-
if (fs.existsSync(enabledPath)) {
|
|
925
|
-
fs.renameSync(enabledPath, disabledPath);
|
|
926
|
-
success(`${hook} disabled`);
|
|
927
|
-
} else if (fs.existsSync(disabledPath)) {
|
|
928
|
-
info(`${hook} is already disabled`);
|
|
929
|
-
} else {
|
|
930
|
-
warning(`${hook} not found`);
|
|
931
|
-
}
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
// Analyze-diff command
|
|
936
|
-
async function analyzeDiff(args) {
|
|
937
|
-
if (!checkGitRepo()) {
|
|
938
|
-
error('You are not in a Git repository.');
|
|
939
|
-
return;
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
// Load configuration
|
|
943
|
-
const config = await getConfig();
|
|
944
|
-
|
|
945
|
-
const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
|
|
946
|
-
|
|
947
|
-
if (!currentBranch) {
|
|
948
|
-
error('You are not in a valid branch.');
|
|
949
|
-
return;
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
// Update remote references
|
|
953
|
-
execSync('git fetch', { stdio: 'ignore' });
|
|
954
|
-
|
|
955
|
-
let baseBranch, compareWith, contextDescription;
|
|
956
|
-
|
|
957
|
-
if (args[0]) {
|
|
958
|
-
// Case with argument: compare current branch vs origin/specified-branch
|
|
959
|
-
const targetBranch = args[0];
|
|
960
|
-
baseBranch = `origin/${targetBranch}`;
|
|
961
|
-
compareWith = `${baseBranch}...HEAD`;
|
|
962
|
-
contextDescription = `${currentBranch} vs ${baseBranch}`;
|
|
963
|
-
|
|
964
|
-
// Check that the origin branch exists
|
|
965
|
-
try {
|
|
966
|
-
execSync(`git rev-parse --verify ${baseBranch}`, { stdio: 'ignore' });
|
|
967
|
-
} catch (e) {
|
|
968
|
-
error(`Branch ${baseBranch} does not exist.`);
|
|
969
|
-
return;
|
|
970
|
-
}
|
|
971
|
-
} else {
|
|
972
|
-
// Case without argument: compare current branch vs origin/current-branch
|
|
973
|
-
baseBranch = `origin/${currentBranch}`;
|
|
974
|
-
compareWith = `${baseBranch}...HEAD`;
|
|
975
|
-
contextDescription = `${currentBranch} vs ${baseBranch}`;
|
|
976
|
-
|
|
977
|
-
// Check that the origin branch exists
|
|
978
|
-
try {
|
|
979
|
-
execSync(`git rev-parse --verify ${baseBranch}`, { stdio: 'ignore' });
|
|
980
|
-
} catch (e) {
|
|
981
|
-
// Try fallback to origin/develop
|
|
982
|
-
baseBranch = 'origin/develop';
|
|
983
|
-
compareWith = `${baseBranch}...HEAD`;
|
|
984
|
-
contextDescription = `${currentBranch} vs ${baseBranch} (fallback)`;
|
|
985
|
-
|
|
986
|
-
try {
|
|
987
|
-
execSync(`git rev-parse --verify ${baseBranch}`, { stdio: 'ignore' });
|
|
988
|
-
warning(`Branch origin/${currentBranch} does not exist. Using ${baseBranch} as fallback.`);
|
|
989
|
-
} catch (e2) {
|
|
990
|
-
// Try fallback to origin/main
|
|
991
|
-
baseBranch = 'origin/main';
|
|
992
|
-
compareWith = `${baseBranch}...HEAD`;
|
|
993
|
-
contextDescription = `${currentBranch} vs ${baseBranch} (fallback)`;
|
|
994
|
-
|
|
995
|
-
try {
|
|
996
|
-
execSync(`git rev-parse --verify ${baseBranch}`, { stdio: 'ignore' });
|
|
997
|
-
warning(`No origin/develop branch. Using ${baseBranch} as fallback.`);
|
|
998
|
-
} catch (e3) {
|
|
999
|
-
error('Could not find a valid comparison branch (tried origin/current, origin/develop, origin/main).');
|
|
1000
|
-
return;
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
info(`Analyzing: ${contextDescription}...`);
|
|
1007
|
-
|
|
1008
|
-
// Get modified files
|
|
1009
|
-
let diffFiles;
|
|
1010
|
-
try {
|
|
1011
|
-
diffFiles = execSync(`git diff ${compareWith} --name-only`, { encoding: 'utf8' }).trim();
|
|
1012
|
-
|
|
1013
|
-
if (!diffFiles) {
|
|
1014
|
-
// Check if there are staged or unstaged changes
|
|
1015
|
-
const stagedFiles = execSync('git diff --cached --name-only', { encoding: 'utf8' }).trim();
|
|
1016
|
-
const unstagedFiles = execSync('git diff --name-only', { encoding: 'utf8' }).trim();
|
|
1017
|
-
|
|
1018
|
-
if (stagedFiles || unstagedFiles) {
|
|
1019
|
-
warning('No differences with remote, but you have uncommitted local changes.');
|
|
1020
|
-
console.log('Staged changes:', stagedFiles || 'none');
|
|
1021
|
-
console.log('Unstaged changes:', unstagedFiles || 'none');
|
|
1022
|
-
} else {
|
|
1023
|
-
success('✅ No differences. Your branch is synchronized.');
|
|
1024
|
-
}
|
|
1025
|
-
return;
|
|
1026
|
-
}
|
|
1027
|
-
} catch (e) {
|
|
1028
|
-
error('Error getting differences: ' + e.message);
|
|
1029
|
-
return;
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
// Get the complete diff
|
|
1033
|
-
let fullDiff, commits;
|
|
1034
|
-
try {
|
|
1035
|
-
if (!args[0] && compareWith.includes('HEAD..')) {
|
|
1036
|
-
fullDiff = execSync(`git diff ${compareWith}`, { encoding: 'utf8' });
|
|
1037
|
-
commits = execSync(`git log ${baseBranch}..HEAD --oneline`, { encoding: 'utf8' }).trim();
|
|
1038
|
-
} else {
|
|
1039
|
-
fullDiff = execSync(`git diff ${compareWith}`, { encoding: 'utf8' });
|
|
1040
|
-
commits = execSync(`git log ${baseBranch}..HEAD --oneline`, { encoding: 'utf8' }).trim();
|
|
1041
|
-
}
|
|
1042
|
-
} catch (e) {
|
|
1043
|
-
error('Error getting diff or commits: ' + e.message);
|
|
1044
|
-
return;
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
// Check if subagents should be used
|
|
1048
|
-
const useSubagents = config.subagents.enabled;
|
|
1049
|
-
const subagentModel = config.subagents.model;
|
|
1050
|
-
let subagentBatchSize = config.subagents.batchSize;
|
|
1051
|
-
// Validate batch size (must be >= 1)
|
|
1052
|
-
if (subagentBatchSize < 1) {
|
|
1053
|
-
subagentBatchSize = 1;
|
|
1054
|
-
}
|
|
1055
|
-
const subagentInstruction = useSubagents
|
|
1056
|
-
? `\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`
|
|
1057
|
-
: '';
|
|
1058
|
-
|
|
1059
|
-
// Truncate full diff if too large
|
|
1060
|
-
const truncatedDiff = fullDiff.length > 50000
|
|
1061
|
-
? fullDiff.substring(0, 50000) + '\n... (truncated diff)'
|
|
1062
|
-
: fullDiff;
|
|
1063
|
-
|
|
1064
|
-
// Load prompt from template
|
|
1065
|
-
const prompt = await loadPrompt('ANALYZE_DIFF.md', {
|
|
1066
|
-
CONTEXT_DESCRIPTION: contextDescription,
|
|
1067
|
-
SUBAGENT_INSTRUCTION: subagentInstruction,
|
|
1068
|
-
COMMITS: commits,
|
|
1069
|
-
DIFF_FILES: diffFiles,
|
|
1070
|
-
FULL_DIFF: truncatedDiff
|
|
1071
|
-
});
|
|
1072
|
-
|
|
1073
|
-
info('Sending to Claude for analysis...');
|
|
1074
|
-
const startTime = Date.now();
|
|
1075
|
-
|
|
1076
|
-
// Prepare telemetry context
|
|
1077
|
-
const filesChanged = diffFiles.split('\n').length;
|
|
1078
|
-
const telemetryContext = {
|
|
1079
|
-
fileCount: filesChanged,
|
|
1080
|
-
batchSize: filesChanged,
|
|
1081
|
-
totalBatches: 1,
|
|
1082
|
-
model: subagentModel || 'sonnet',
|
|
1083
|
-
hook: 'analyze-diff'
|
|
1084
|
-
};
|
|
1085
|
-
|
|
1086
|
-
try {
|
|
1087
|
-
// Use cross-platform executeClaudeWithRetry from claude-client.js with telemetry
|
|
1088
|
-
const response = await executeClaudeWithRetry(prompt, {
|
|
1089
|
-
timeout: 180000, // 3 minutes for diff analysis
|
|
1090
|
-
telemetryContext
|
|
1091
|
-
});
|
|
1092
|
-
|
|
1093
|
-
// Extract JSON from response using claude-client utility
|
|
1094
|
-
const result = extractJSON(response);
|
|
1095
|
-
|
|
1096
|
-
// Show the results
|
|
1097
|
-
console.log('');
|
|
1098
|
-
console.log('════════════════════════════════════════════════════════════════');
|
|
1099
|
-
console.log(' DIFFERENCES ANALYSIS ');
|
|
1100
|
-
console.log('════════════════════════════════════════════════════════════════');
|
|
1101
|
-
console.log('');
|
|
1102
|
-
|
|
1103
|
-
console.log(`🔍 ${colors.blue}Context:${colors.reset} ${contextDescription}`);
|
|
1104
|
-
console.log(`📊 ${colors.blue}Changed Files:${colors.reset} ${diffFiles.split('\n').length}`);
|
|
1105
|
-
console.log('');
|
|
1106
|
-
|
|
1107
|
-
console.log(`📝 ${colors.green}Pull Request Title:${colors.reset}`);
|
|
1108
|
-
console.log(` ${result.prTitle}`);
|
|
1109
|
-
console.log('');
|
|
1110
|
-
|
|
1111
|
-
console.log(`🌿 ${colors.green}Suggested branch name:${colors.reset}`);
|
|
1112
|
-
console.log(` ${result.suggestedBranchName}`);
|
|
1113
|
-
console.log('');
|
|
1114
|
-
|
|
1115
|
-
console.log(`📋 ${colors.green}Type of change:${colors.reset} ${result.changeType}`);
|
|
1116
|
-
|
|
1117
|
-
if (result.breakingChanges) {
|
|
1118
|
-
console.log(`⚠️ ${colors.yellow}Breaking Changes: SÍ${colors.reset}`);
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
console.log('');
|
|
1122
|
-
console.log(`📄 ${colors.green}Pull Request Description:${colors.reset}`);
|
|
1123
|
-
console.log('───────────────────────────────────────────────────────────────');
|
|
1124
|
-
console.log(result.prDescription);
|
|
1125
|
-
console.log('───────────────────────────────────────────────────────────────');
|
|
1126
|
-
|
|
1127
|
-
if (result.testingNotes) {
|
|
1128
|
-
console.log('');
|
|
1129
|
-
console.log(`🧪 ${colors.green}Testing notes:${colors.reset}`);
|
|
1130
|
-
console.log(result.testingNotes);
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
// Guardar los resultados en un archivo con contexto
|
|
1134
|
-
const outputData = {
|
|
1135
|
-
...result,
|
|
1136
|
-
context: {
|
|
1137
|
-
currentBranch,
|
|
1138
|
-
baseBranch,
|
|
1139
|
-
contextDescription,
|
|
1140
|
-
filesChanged: diffFiles.split('\n').length,
|
|
1141
|
-
timestamp: new Date().toISOString()
|
|
1142
|
-
}
|
|
1143
|
-
};
|
|
1144
|
-
|
|
1145
|
-
// Ensure .claude/out directory exists
|
|
1146
|
-
const outputDir = '.claude/out';
|
|
1147
|
-
if (!fs.existsSync(outputDir)) {
|
|
1148
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
const outputFile = '.claude/out/pr-analysis.json';
|
|
1152
|
-
fs.writeFileSync(outputFile, JSON.stringify(outputData, null, 2));
|
|
1153
|
-
|
|
1154
|
-
const elapsed = Date.now() - startTime;
|
|
1155
|
-
const seconds = Math.floor(elapsed / 1000);
|
|
1156
|
-
const ms = elapsed % 1000;
|
|
1157
|
-
console.log('');
|
|
1158
|
-
console.log(`${colors.blue}⏱️ Analysis completed in ${seconds}.${ms}s${colors.reset}`);
|
|
1159
|
-
info(`Results saved in ${outputFile}`);
|
|
1160
|
-
|
|
1161
|
-
// Sugerencias contextuales
|
|
1162
|
-
console.log('');
|
|
1163
|
-
if (!args[0] && contextDescription.includes('local changes without push')) {
|
|
1164
|
-
// Case of local changes without push
|
|
1165
|
-
console.log(`💡 ${colors.yellow}To create new branch with these changes:${colors.reset}`);
|
|
1166
|
-
console.log(` git checkout -b ${result.suggestedBranchName}`);
|
|
1167
|
-
console.log(` git push -u origin ${result.suggestedBranchName}`);
|
|
1168
|
-
} else if (currentBranch !== result.suggestedBranchName) {
|
|
1169
|
-
// Caso normal de comparación entre ramas
|
|
1170
|
-
console.log(`💡 ${colors.yellow}For renaming your current branch:${colors.reset}`);
|
|
1171
|
-
console.log(` git branch -m ${result.suggestedBranchName}`);
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
console.log(`💡 ${colors.yellow}Tip:${colors.reset} Use this information to create your PR on GitHub.`);
|
|
1175
|
-
|
|
1176
|
-
} catch (e) {
|
|
1177
|
-
error('Error executing Claude: ' + e.message);
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
// Create PR command (v2.5.0+ - Octokit-based)
|
|
1182
|
-
async function createPr(args) {
|
|
1183
|
-
logger.debug('create-pr', 'Starting create-pr command', { args });
|
|
1184
|
-
|
|
1185
|
-
if (!checkGitRepo()) {
|
|
1186
|
-
error('You are not in a Git repository.');
|
|
1187
|
-
logger.debug('create-pr', 'Not in a git repository, exiting');
|
|
1188
|
-
return;
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
try {
|
|
1192
|
-
// Load configuration
|
|
1193
|
-
logger.debug('create-pr', 'Loading configuration');
|
|
1194
|
-
const config = await getConfig();
|
|
1195
|
-
logger.debug('create-pr', 'Configuration loaded', {
|
|
1196
|
-
preset: config.preset,
|
|
1197
|
-
githubEnabled: config.github?.enabled,
|
|
1198
|
-
defaultBase: config.github?.pr?.defaultBase
|
|
1199
|
-
});
|
|
1200
|
-
|
|
1201
|
-
// Import GitHub API module
|
|
1202
|
-
logger.debug('create-pr', 'Importing GitHub API modules');
|
|
1203
|
-
const { createPullRequest, GitHubAPIError, validateToken, findExistingPR } = await import('../lib/utils/github-api.js');
|
|
1204
|
-
const { parseGitHubRepo } = await import('../lib/utils/github-client.js');
|
|
1205
|
-
|
|
1206
|
-
showInfo('🚀 Creating Pull Request...');
|
|
1207
|
-
console.log('');
|
|
1208
|
-
|
|
1209
|
-
// Step 1: Validate GitHub token
|
|
1210
|
-
logger.debug('create-pr', 'Step 1: Validating GitHub token');
|
|
1211
|
-
const tokenValidation = await validateToken();
|
|
1212
|
-
if (!tokenValidation.valid) {
|
|
1213
|
-
logger.error('create-pr', 'GitHub authentication failed', { error: tokenValidation.error });
|
|
1214
|
-
showError('GitHub authentication failed');
|
|
1215
|
-
console.log('');
|
|
1216
|
-
console.log('Please configure your GitHub token:');
|
|
1217
|
-
console.log(' Option 1: Set GITHUB_TOKEN environment variable');
|
|
1218
|
-
console.log(' Option 2: Add token to .claude/settings.local.json:');
|
|
1219
|
-
console.log(' { "githubToken": "ghp_your_token_here" }');
|
|
1220
|
-
console.log(' Option 3: Run: claude-hooks setup-github');
|
|
1221
|
-
console.log('');
|
|
1222
|
-
process.exit(1);
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
logger.debug('create-pr', 'Token validation successful', {
|
|
1226
|
-
user: tokenValidation.user,
|
|
1227
|
-
hasRepoScope: tokenValidation.hasRepoScope,
|
|
1228
|
-
scopes: tokenValidation.scopes
|
|
1229
|
-
});
|
|
1230
|
-
|
|
1231
|
-
showSuccess(`Authenticated as: ${tokenValidation.user}`);
|
|
1232
|
-
if (!tokenValidation.hasRepoScope) {
|
|
1233
|
-
showWarning('Token may lack "repo" scope - PR creation might fail');
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
// Step 2: Get or prompt for task-id (with config for pattern)
|
|
1237
|
-
logger.debug('create-pr', 'Step 2: Getting or prompting for task-id');
|
|
1238
|
-
const taskId = await getOrPromptTaskId({
|
|
1239
|
-
prompt: true, // DO prompt for PRs (unlike commit messages)
|
|
1240
|
-
required: false, // Allow skipping
|
|
1241
|
-
config: config // Pass config for custom pattern
|
|
1242
|
-
});
|
|
1243
|
-
logger.debug('create-pr', 'Task ID determined', { taskId });
|
|
1244
|
-
|
|
1245
|
-
// Step 3: Parse arguments and determine base branch
|
|
1246
|
-
logger.debug('create-pr', 'Step 3: Parsing arguments and determining base branch', { args });
|
|
1247
|
-
let baseBranchArg = args[0];
|
|
1248
|
-
if (baseBranchArg && /^[A-Z]{2,10}-\d+$/i.test(baseBranchArg)) {
|
|
1249
|
-
baseBranchArg = args[1];
|
|
1250
|
-
}
|
|
1251
|
-
const baseBranch = baseBranchArg || config.github?.pr?.defaultBase || 'develop';
|
|
1252
|
-
logger.debug('create-pr', 'Base branch determined', { baseBranch, fromConfig: !baseBranchArg });
|
|
1253
|
-
|
|
1254
|
-
// Step 4: Get current branch and repo info
|
|
1255
|
-
logger.debug('create-pr', 'Step 4: Getting current branch and repo info');
|
|
1256
|
-
const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
|
|
1257
|
-
if (!currentBranch) {
|
|
1258
|
-
logger.error('create-pr', 'Could not determine current branch');
|
|
1259
|
-
error('Could not determine current branch');
|
|
1260
|
-
return;
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
const repoInfo = parseGitHubRepo();
|
|
1264
|
-
logger.debug('create-pr', 'Repository and branch info', {
|
|
1265
|
-
owner: repoInfo.owner,
|
|
1266
|
-
repo: repoInfo.repo,
|
|
1267
|
-
currentBranch,
|
|
1268
|
-
baseBranch
|
|
1269
|
-
});
|
|
1270
|
-
|
|
1271
|
-
showInfo(`Repository: ${repoInfo.fullName}`);
|
|
1272
|
-
showInfo(`Branch: ${currentBranch} → ${baseBranch}`);
|
|
1273
|
-
|
|
1274
|
-
// Step 5: Check for existing PR
|
|
1275
|
-
logger.debug('create-pr', 'Step 5: Checking for existing PR');
|
|
1276
|
-
const existingPR = await findExistingPR({
|
|
1277
|
-
owner: repoInfo.owner,
|
|
1278
|
-
repo: repoInfo.repo,
|
|
1279
|
-
head: currentBranch,
|
|
1280
|
-
base: baseBranch
|
|
1281
|
-
});
|
|
1282
|
-
|
|
1283
|
-
if (existingPR) {
|
|
1284
|
-
logger.debug('create-pr', 'Existing PR found, exiting', {
|
|
1285
|
-
prNumber: existingPR.number,
|
|
1286
|
-
prUrl: existingPR.html_url
|
|
1287
|
-
});
|
|
1288
|
-
showWarning(`A PR already exists for this branch: #${existingPR.number}`);
|
|
1289
|
-
console.log(` ${existingPR.html_url}`);
|
|
1290
|
-
console.log('');
|
|
1291
|
-
return;
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
logger.debug('create-pr', 'No existing PR found, continuing');
|
|
1295
|
-
|
|
1296
|
-
// Step 6: Update remote and check for differences
|
|
1297
|
-
logger.debug('create-pr', 'Step 6: Fetching latest changes from remote');
|
|
1298
|
-
execSync('git fetch', { stdio: 'ignore' });
|
|
1299
|
-
const compareWith = `origin/${baseBranch}...HEAD`;
|
|
1300
|
-
|
|
1301
|
-
try {
|
|
1302
|
-
execSync(`git rev-parse --verify origin/${baseBranch}`, { stdio: 'ignore' });
|
|
1303
|
-
} catch (e) {
|
|
1304
|
-
error(`Base branch origin/${baseBranch} does not exist`);
|
|
1305
|
-
return;
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
let diffFiles;
|
|
1309
|
-
try {
|
|
1310
|
-
diffFiles = execSync(`git diff ${compareWith} --name-only`, { encoding: 'utf8' }).trim();
|
|
1311
|
-
if (!diffFiles) {
|
|
1312
|
-
showWarning('No differences with remote branch. Nothing to create a PR for.');
|
|
1313
|
-
return;
|
|
1314
|
-
}
|
|
1315
|
-
} catch (e) {
|
|
1316
|
-
error('Error getting differences: ' + e.message);
|
|
1317
|
-
return;
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
const filesArray = diffFiles.split('\n').filter(f => f.trim());
|
|
1321
|
-
logger.debug('create-pr', 'Modified files detected', {
|
|
1322
|
-
fileCount: filesArray.length,
|
|
1323
|
-
files: filesArray
|
|
1324
|
-
});
|
|
1325
|
-
showInfo(`Found ${filesArray.length} modified file(s)`);
|
|
1326
|
-
|
|
1327
|
-
// Step 7: Generate PR metadata with Claude (reuse analyze-diff logic)
|
|
1328
|
-
logger.debug('create-pr', 'Step 7: Generating PR metadata with Claude');
|
|
1329
|
-
let fullDiff, commits;
|
|
1330
|
-
try {
|
|
1331
|
-
fullDiff = execSync(`git diff ${compareWith}`, { encoding: 'utf8' });
|
|
1332
|
-
commits = execSync(`git log origin/${baseBranch}..HEAD --oneline`, { encoding: 'utf8' }).trim();
|
|
1333
|
-
} catch (e) {
|
|
1334
|
-
error('Error getting diff or commits: ' + e.message);
|
|
1335
|
-
return;
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
const truncatedDiff = fullDiff.length > 50000
|
|
1339
|
-
? fullDiff.substring(0, 50000) + '\n... (truncated)'
|
|
1340
|
-
: fullDiff;
|
|
1341
|
-
|
|
1342
|
-
const contextDescription = `${currentBranch} vs origin/${baseBranch}`;
|
|
1343
|
-
const prompt = await loadPrompt('ANALYZE_DIFF.md', {
|
|
1344
|
-
CONTEXT_DESCRIPTION: contextDescription,
|
|
1345
|
-
SUBAGENT_INSTRUCTION: '',
|
|
1346
|
-
COMMITS: commits,
|
|
1347
|
-
DIFF_FILES: diffFiles,
|
|
1348
|
-
FULL_DIFF: truncatedDiff
|
|
1349
|
-
});
|
|
1350
|
-
|
|
1351
|
-
showInfo('Generating PR metadata with Claude...');
|
|
1352
|
-
logger.debug('create-pr', 'Calling Claude with prompt', { promptLength: prompt.length });
|
|
1353
|
-
|
|
1354
|
-
// Prepare telemetry context for create-pr
|
|
1355
|
-
const telemetryContext = {
|
|
1356
|
-
fileCount: filesArray.length,
|
|
1357
|
-
batchSize: filesArray.length,
|
|
1358
|
-
totalBatches: 1,
|
|
1359
|
-
model: 'sonnet', // create-pr always uses main model
|
|
1360
|
-
hook: 'create-pr'
|
|
1361
|
-
};
|
|
1362
|
-
|
|
1363
|
-
const response = await executeClaudeWithRetry(prompt, {
|
|
1364
|
-
timeout: 180000,
|
|
1365
|
-
telemetryContext
|
|
1366
|
-
});
|
|
1367
|
-
logger.debug('create-pr', 'Claude response received', { responseLength: response.length });
|
|
1368
|
-
|
|
1369
|
-
const analysisResult = extractJSON(response);
|
|
1370
|
-
logger.debug('create-pr', 'Analysis result extracted', {
|
|
1371
|
-
hasResult: !!analysisResult,
|
|
1372
|
-
hasPrTitle: !!analysisResult?.prTitle
|
|
1373
|
-
});
|
|
1374
|
-
|
|
1375
|
-
if (!analysisResult || !analysisResult.prTitle) {
|
|
1376
|
-
logger.error('create-pr', 'Failed to generate PR metadata from analysis', { analysisResult });
|
|
1377
|
-
error('Failed to generate PR metadata from analysis');
|
|
1378
|
-
return;
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
// Step 8: Prepare PR data
|
|
1382
|
-
logger.debug('create-pr', 'Step 8: Preparing PR data');
|
|
1383
|
-
let prTitle = analysisResult.prTitle;
|
|
1384
|
-
if (taskId) {
|
|
1385
|
-
prTitle = formatWithTaskId(prTitle, taskId);
|
|
1386
|
-
logger.debug('create-pr', 'Task ID added to title', { prTitle });
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
const prBody = analysisResult.prDescription || analysisResult.description || '';
|
|
1390
|
-
logger.debug('create-pr', 'PR title and body prepared', {
|
|
1391
|
-
titleLength: prTitle.length,
|
|
1392
|
-
bodyLength: prBody.length
|
|
1393
|
-
});
|
|
1394
|
-
|
|
1395
|
-
// Step 9: Get labels from preset
|
|
1396
|
-
logger.debug('create-pr', 'Step 9: Getting labels from preset');
|
|
1397
|
-
let labels = [];
|
|
1398
|
-
if (config.preset && config.github?.pr?.labelRules) {
|
|
1399
|
-
labels = config.github.pr.labelRules[config.preset] || [];
|
|
1400
|
-
}
|
|
1401
|
-
if (analysisResult.breakingChanges) {
|
|
1402
|
-
labels.push('breaking-change');
|
|
1403
|
-
}
|
|
1404
|
-
logger.debug('create-pr', 'Labels determined', { labels, preset: config.preset });
|
|
1405
|
-
|
|
1406
|
-
// Step 10: Get reviewers from CODEOWNERS and config
|
|
1407
|
-
logger.debug('create-pr', 'Step 10: Getting reviewers from CODEOWNERS and config');
|
|
1408
|
-
const reviewers = await getReviewersForFiles(filesArray, config.github?.pr);
|
|
1409
|
-
logger.debug('create-pr', 'Reviewers determined', { reviewers, sources: 'CODEOWNERS + config' });
|
|
1410
|
-
|
|
1411
|
-
// Step 11: Show PR preview
|
|
1412
|
-
const prData = {
|
|
1413
|
-
title: prTitle,
|
|
1414
|
-
body: prBody,
|
|
1415
|
-
head: currentBranch,
|
|
1416
|
-
base: baseBranch,
|
|
1417
|
-
labels,
|
|
1418
|
-
reviewers
|
|
1419
|
-
};
|
|
1420
|
-
|
|
1421
|
-
showPRPreview(prData);
|
|
1422
|
-
|
|
1423
|
-
// Step 12: Prompt for confirmation
|
|
1424
|
-
const action = await promptMenu(
|
|
1425
|
-
'What would you like to do?',
|
|
1426
|
-
[
|
|
1427
|
-
{ key: 'c', label: 'Create PR' },
|
|
1428
|
-
{ key: 'x', label: 'Cancel' }
|
|
1429
|
-
],
|
|
1430
|
-
'c'
|
|
1431
|
-
);
|
|
1432
|
-
|
|
1433
|
-
if (action === 'x') {
|
|
1434
|
-
showInfo('PR creation cancelled');
|
|
1435
|
-
|
|
1436
|
-
// Save metadata for later use
|
|
1437
|
-
const outputDir = '.claude/out';
|
|
1438
|
-
if (!fs.existsSync(outputDir)) {
|
|
1439
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
1440
|
-
}
|
|
1441
|
-
const outputFile = path.join(outputDir, 'pr-metadata.json');
|
|
1442
|
-
fs.writeFileSync(outputFile, JSON.stringify(prData, null, 2));
|
|
1443
|
-
showInfo(`PR metadata saved to ${outputFile}`);
|
|
1444
|
-
return;
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
// Step 13: Create PR via Octokit
|
|
1448
|
-
logger.debug('create-pr', 'Step 13: Creating PR via Octokit');
|
|
1449
|
-
showInfo('Creating pull request on GitHub...');
|
|
1450
|
-
|
|
1451
|
-
try {
|
|
1452
|
-
logger.debug('create-pr', 'Calling createPullRequest API', {
|
|
1453
|
-
owner: repoInfo.owner,
|
|
1454
|
-
repo: repoInfo.repo,
|
|
1455
|
-
head: prData.head,
|
|
1456
|
-
base: prData.base
|
|
1457
|
-
});
|
|
1458
|
-
|
|
1459
|
-
const result = await createPullRequest({
|
|
1460
|
-
owner: repoInfo.owner,
|
|
1461
|
-
repo: repoInfo.repo,
|
|
1462
|
-
title: prData.title,
|
|
1463
|
-
body: prData.body,
|
|
1464
|
-
head: prData.head,
|
|
1465
|
-
base: prData.base,
|
|
1466
|
-
draft: false,
|
|
1467
|
-
labels: prData.labels,
|
|
1468
|
-
reviewers: prData.reviewers
|
|
1469
|
-
});
|
|
1470
|
-
|
|
1471
|
-
logger.debug('create-pr', 'PR created successfully', {
|
|
1472
|
-
prNumber: result.number,
|
|
1473
|
-
prUrl: result.html_url
|
|
1474
|
-
});
|
|
1475
|
-
|
|
1476
|
-
console.log('');
|
|
1477
|
-
showSuccess('Pull request created successfully!');
|
|
1478
|
-
console.log('');
|
|
1479
|
-
console.log(` PR #${result.number}: ${result.html_url}`);
|
|
1480
|
-
console.log('');
|
|
1481
|
-
|
|
1482
|
-
if (result.reviewers.length > 0) {
|
|
1483
|
-
showInfo(`Reviewers requested: ${result.reviewers.join(', ')}`);
|
|
1484
|
-
}
|
|
1485
|
-
if (result.labels.length > 0) {
|
|
1486
|
-
showInfo(`Labels added: ${result.labels.join(', ')}`);
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
} catch (apiError) {
|
|
1490
|
-
logger.error('create-pr', 'Failed to create pull request', apiError);
|
|
1491
|
-
showError('Failed to create pull request');
|
|
1492
|
-
console.error('');
|
|
1493
|
-
console.error(` ${apiError.message}`);
|
|
1494
|
-
|
|
1495
|
-
if (apiError.context?.suggestion) {
|
|
1496
|
-
console.error('');
|
|
1497
|
-
console.error(` 💡 ${apiError.context.suggestion}`);
|
|
1498
|
-
}
|
|
1499
|
-
console.error('');
|
|
1500
|
-
|
|
1501
|
-
// Save PR metadata for manual creation or retry
|
|
1502
|
-
const outputDir = '.claude/out';
|
|
1503
|
-
if (!fs.existsSync(outputDir)) {
|
|
1504
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
1505
|
-
}
|
|
1506
|
-
const outputFile = path.join(outputDir, 'pr-metadata.json');
|
|
1507
|
-
fs.writeFileSync(outputFile, JSON.stringify({
|
|
1508
|
-
...prData,
|
|
1509
|
-
error: apiError.message,
|
|
1510
|
-
timestamp: new Date().toISOString()
|
|
1511
|
-
}, null, 2));
|
|
1512
|
-
|
|
1513
|
-
logger.debug('create-pr', 'PR metadata saved', { outputFile });
|
|
1514
|
-
showInfo(`PR metadata saved to ${outputFile}`);
|
|
1515
|
-
showInfo('You can create the PR manually using this data');
|
|
1516
|
-
|
|
1517
|
-
process.exit(1);
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1520
|
-
} catch (err) {
|
|
1521
|
-
logger.error('create-pr', 'Error creating PR', err);
|
|
1522
|
-
showError('Error creating PR: ' + err.message);
|
|
1523
|
-
|
|
1524
|
-
if (err.context) {
|
|
1525
|
-
logger.debug('create-pr', 'Error context', err.context);
|
|
1526
|
-
console.error('Context:', JSON.stringify(err.context, null, 2));
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
process.exit(1);
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
|
|
1533
|
-
// Setup GitHub authentication
|
|
1534
|
-
async function setupGitHub() {
|
|
1535
|
-
const { validateToken } = await import('../lib/utils/github-api.js');
|
|
1536
|
-
|
|
1537
|
-
console.log('');
|
|
1538
|
-
info('GitHub Authentication Setup');
|
|
1539
|
-
console.log('');
|
|
1540
|
-
|
|
1541
|
-
// Check existing token
|
|
1542
|
-
try {
|
|
1543
|
-
const validation = await validateToken();
|
|
1544
|
-
if (validation.valid) {
|
|
1545
|
-
success(`Already authenticated as: ${validation.user}`);
|
|
1546
|
-
console.log(` Scopes: ${validation.scopes.join(', ')}`);
|
|
1547
|
-
|
|
1548
|
-
if (!validation.hasRepoScope) {
|
|
1549
|
-
warning('Token lacks "repo" scope - PR creation may fail');
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
console.log('');
|
|
1553
|
-
info('To use a different token, edit .claude/settings.local.json');
|
|
1554
|
-
return;
|
|
1555
|
-
}
|
|
1556
|
-
} catch (e) {
|
|
1557
|
-
// No token configured, continue with setup
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
console.log('No GitHub token found. You have several options:');
|
|
1561
|
-
console.log('');
|
|
1562
|
-
console.log('Option 1: Create .claude/settings.local.json');
|
|
1563
|
-
console.log(' {');
|
|
1564
|
-
console.log(' "githubToken": "ghp_your_token_here"');
|
|
1565
|
-
console.log(' }');
|
|
1566
|
-
console.log('');
|
|
1567
|
-
console.log('Option 2: Set environment variable');
|
|
1568
|
-
console.log(' export GITHUB_TOKEN="ghp_your_token_here"');
|
|
1569
|
-
console.log('');
|
|
1570
|
-
console.log('Option 3: Run setup-mcp (if you also want MCP features)');
|
|
1571
|
-
console.log(' claude-hooks setup-mcp');
|
|
1572
|
-
console.log('');
|
|
1573
|
-
console.log('To create a token:');
|
|
1574
|
-
console.log(' 1. Go to https://github.com/settings/tokens/new');
|
|
1575
|
-
console.log(' 2. Select scopes: repo, read:org');
|
|
1576
|
-
console.log(' 3. Generate and copy the token');
|
|
1577
|
-
console.log('');
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
// Comando status
|
|
1581
|
-
function status() {
|
|
1582
|
-
if (!checkGitRepo()) {
|
|
1583
|
-
error('You are not in a Git repository.');
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
info('Claude Git Hooks status:\n');
|
|
1587
|
-
|
|
1588
|
-
const hooks = ['pre-commit', 'prepare-commit-msg'];
|
|
1589
|
-
hooks.forEach(hook => {
|
|
1590
|
-
const enabledPath = `.git/hooks/${hook}`;
|
|
1591
|
-
const disabledPath = `.git/hooks/${hook}.disabled`;
|
|
1592
|
-
|
|
1593
|
-
if (fs.existsSync(enabledPath)) {
|
|
1594
|
-
success(`${hook}: enabled`);
|
|
1595
|
-
} else if (fs.existsSync(disabledPath)) {
|
|
1596
|
-
warning(`${hook}: disabled`);
|
|
1597
|
-
} else {
|
|
1598
|
-
error(`${hook}: not installed`);
|
|
1599
|
-
}
|
|
1600
|
-
});
|
|
1601
|
-
|
|
1602
|
-
// Check guidelines files
|
|
1603
|
-
console.log('\nGuidelines files:');
|
|
1604
|
-
const guidelines = ['CLAUDE_PRE_COMMIT.md'];
|
|
1605
|
-
guidelines.forEach(guideline => {
|
|
1606
|
-
const claudePath = path.join('.claude', guideline);
|
|
1607
|
-
if (fs.existsSync(claudePath)) {
|
|
1608
|
-
success(`${guideline}: present in .claude/`);
|
|
1609
|
-
} else if (fs.existsSync(guideline)) {
|
|
1610
|
-
warning(`${guideline}: present in root (should be in .claude/)`);
|
|
1611
|
-
} else {
|
|
1612
|
-
warning(`${guideline}: missing`);
|
|
1613
|
-
}
|
|
1614
|
-
});
|
|
1615
|
-
|
|
1616
|
-
// Verificar entradas en .gitignore
|
|
1617
|
-
console.log('\n.gitignore:');
|
|
1618
|
-
const gitignorePath = '.gitignore';
|
|
1619
|
-
if (fs.existsSync(gitignorePath)) {
|
|
1620
|
-
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
|
1621
|
-
const claudeIgnore = '.claude/';
|
|
1622
|
-
|
|
1623
|
-
const regex = new RegExp(`^${claudeIgnore.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm');
|
|
1624
|
-
if (regex.test(gitignoreContent)) {
|
|
1625
|
-
success(`${claudeIgnore}: included (protects all Claude files)`);
|
|
1626
|
-
} else {
|
|
1627
|
-
warning(`${claudeIgnore}: missing`);
|
|
1628
|
-
info('\nRun "claude-hooks install" to update .gitignore');
|
|
1629
|
-
}
|
|
1630
|
-
} else {
|
|
1631
|
-
warning('.gitignore doesn´t exist');
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
// Cross-platform version comparison (semver)
|
|
1636
|
-
// Why: Pure JavaScript, no bash dependency
|
|
1637
|
-
// Returns: 0 if equal, 1 if v1 > v2, -1 if v1 < v2
|
|
1638
|
-
function compareVersions(v1, v2) {
|
|
1639
|
-
if (v1 === v2) return 0;
|
|
1640
|
-
|
|
1641
|
-
const v1Parts = v1.split('.').map(Number);
|
|
1642
|
-
const v2Parts = v2.split('.').map(Number);
|
|
1643
|
-
|
|
1644
|
-
for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
|
|
1645
|
-
const v1Part = v1Parts[i] || 0;
|
|
1646
|
-
const v2Part = v2Parts[i] || 0;
|
|
1647
|
-
|
|
1648
|
-
if (v1Part > v2Part) return 1;
|
|
1649
|
-
if (v1Part < v2Part) return -1;
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
return 0;
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
// Update command - update to the latest version
|
|
1656
|
-
async function update() {
|
|
1657
|
-
info('Checking latest available version...');
|
|
1658
|
-
|
|
1659
|
-
try {
|
|
1660
|
-
const currentVersion = getPackageJson().version;
|
|
1661
|
-
const latestVersion = await getLatestVersion('claude-git-hooks');
|
|
1662
|
-
|
|
1663
|
-
const comparison = compareVersions(currentVersion, latestVersion);
|
|
1664
|
-
|
|
1665
|
-
if (comparison === 0) {
|
|
1666
|
-
success(`You already have the latest version installed (${currentVersion})`);
|
|
1667
|
-
return;
|
|
1668
|
-
} else if (comparison > 0) {
|
|
1669
|
-
info(`You are using a development version (${currentVersion})`);
|
|
1670
|
-
info(`Latest published version: ${latestVersion}`);
|
|
1671
|
-
success(`You already have the latest version installed (${currentVersion})`);
|
|
1672
|
-
return;
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
info(`Current version: ${currentVersion}`);
|
|
1676
|
-
info(`Available version: ${latestVersion}`);
|
|
1677
|
-
|
|
1678
|
-
// Actualizar el paquete
|
|
1679
|
-
info('Updating claude-git-hooks...');
|
|
1680
|
-
try {
|
|
1681
|
-
execSync('npm install -g claude-git-hooks@latest', { stdio: 'inherit' });
|
|
1682
|
-
success(`Successfully updated to version ${latestVersion}`);
|
|
1683
|
-
|
|
1684
|
-
// Reinstall hooks with the new version
|
|
1685
|
-
info('Reinstalling hooks with the new version...');
|
|
1686
|
-
await install(['--force']);
|
|
1687
|
-
|
|
1688
|
-
} catch (updateError) {
|
|
1689
|
-
error('Error updating. Try running: npm install -g claude-git-hooks@latest');
|
|
1690
|
-
}
|
|
1691
|
-
} catch (e) {
|
|
1692
|
-
warning('Could not check the latest available version');
|
|
1693
|
-
warning('Trying to update anyway...');
|
|
1694
|
-
try {
|
|
1695
|
-
execSync('npm install -g claude-git-hooks@latest', { stdio: 'inherit' });
|
|
1696
|
-
success('Update completed');
|
|
1697
|
-
await install(['--force']);
|
|
1698
|
-
} catch (updateError) {
|
|
1699
|
-
error('Error updating: ' + updateError.message);
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
// Show version command
|
|
1705
|
-
// Why: Reusable function to display current version from package.json
|
|
1706
|
-
function showVersion() {
|
|
1707
|
-
const pkg = getPackageJson();
|
|
1708
|
-
console.log(`${pkg.name} v${pkg.version}`);
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
// Comando help
|
|
1712
|
-
function showHelp() {
|
|
1713
|
-
console.log(`
|
|
1714
|
-
Claude Git Hooks - Code analysis and automatic messages with Claude CLI
|
|
1715
|
-
|
|
1716
|
-
Usage: claude-hooks <command> [options]
|
|
1717
|
-
|
|
1718
|
-
Commands:
|
|
1719
|
-
install [options] Install hooks in the current repository
|
|
1720
|
-
--force Reinstall even if they already exist
|
|
1721
|
-
--skip-auth Skip Claude authentication verification
|
|
1722
|
-
update Update to the latest available version
|
|
1723
|
-
uninstall Uninstall hooks from the repository
|
|
1724
|
-
enable [hook] Enable hooks (all or one specific)
|
|
1725
|
-
disable [hook] Disable hooks (all or one specific)
|
|
1726
|
-
status Show the status of hooks
|
|
1727
|
-
analyze-diff [base] Analyze differences between branches and generate PR info
|
|
1728
|
-
create-pr [base] Create pull request with auto-generated metadata and reviewers
|
|
1729
|
-
setup-github Setup GitHub login (required for create-pr)
|
|
1730
|
-
presets List all available presets
|
|
1731
|
-
--set-preset <name> Set the active preset
|
|
1732
|
-
preset current Show the current active preset
|
|
1733
|
-
telemetry [action] Telemetry management (show or clear)
|
|
1734
|
-
--debug <value> Set debug mode (true, false, or status)
|
|
1735
|
-
--version, -v Show the current version
|
|
1736
|
-
help Show this help
|
|
1737
|
-
|
|
1738
|
-
Available hooks:
|
|
1739
|
-
pre-commit Code analysis before commit
|
|
1740
|
-
prepare-commit-msg Automatic message generation
|
|
1741
|
-
|
|
1742
|
-
Examples:
|
|
1743
|
-
claude-hooks install # Install all hooks
|
|
1744
|
-
claude-hooks install --skip-auth # Install without verifying authentication
|
|
1745
|
-
claude-hooks update # Update to the latest version
|
|
1746
|
-
claude-hooks disable pre-commit # Disable only pre-commit
|
|
1747
|
-
claude-hooks enable # Enable all hooks
|
|
1748
|
-
claude-hooks status # View current status
|
|
1749
|
-
claude-hooks analyze-diff main # Analyze differences with main
|
|
1750
|
-
claude-hooks setup-github # Configure GitHub authentication for PR creation
|
|
1751
|
-
claude-hooks setup-mcp # Setup GitHub MCP (one-time setup)
|
|
1752
|
-
claude-hooks create-pr develop # Create PR targeting develop branch
|
|
1753
|
-
claude-hooks presets # List available presets
|
|
1754
|
-
claude-hooks --set-preset backend # Set backend preset
|
|
1755
|
-
claude-hooks preset current # Show current preset
|
|
1756
|
-
claude-hooks telemetry show # Show telemetry statistics
|
|
1757
|
-
claude-hooks telemetry clear # Clear telemetry data
|
|
1758
|
-
claude-hooks --debug true # Enable debug mode
|
|
1759
|
-
claude-hooks --debug status # Check debug status
|
|
1760
|
-
|
|
1761
|
-
Commit use cases:
|
|
1762
|
-
git commit -m "message" # Manual message + blocking analysis
|
|
1763
|
-
git commit -m "auto" # Automatic message + blocking analysis
|
|
1764
|
-
git commit --no-verify -m "auto" # Automatic message without analysis
|
|
1765
|
-
git commit --no-verify -m "msg" # Manual message without analysis
|
|
1766
|
-
|
|
1767
|
-
Analyze-diff use case:
|
|
1768
|
-
claude-hooks analyze-diff main # Analyze changes vs main and generate:
|
|
1769
|
-
→ PR Title: "feat: add user authentication module"
|
|
1770
|
-
→ PR Description: "## Summary\n- Added JWT authentication..."
|
|
1771
|
-
→ Suggested branch: "feature/user-authentication"
|
|
1772
|
-
|
|
1773
|
-
Create-pr use case (v2.5.0+):
|
|
1774
|
-
claude-hooks create-pr develop # Create PR targeting develop:
|
|
1775
|
-
→ Validates GitHub token
|
|
1776
|
-
→ Extracts task-id from branch (IX-123, #456, LIN-123)
|
|
1777
|
-
→ Analyzes diff and generates PR metadata with Claude
|
|
1778
|
-
→ Creates PR directly via GitHub API (Octokit)
|
|
1779
|
-
→ Adds labels based on preset
|
|
1780
|
-
→ Returns PR URL
|
|
1781
|
-
|
|
1782
|
-
Token configuration:
|
|
1783
|
-
→ .claude/settings.local.json (recommended, gitignored)
|
|
1784
|
-
→ GITHUB_TOKEN environment variable
|
|
1785
|
-
→ Claude Desktop config (auto-detected)
|
|
1786
|
-
|
|
1787
|
-
Presets (v2.3.0+):
|
|
1788
|
-
Built-in tech-stack specific configurations:
|
|
1789
|
-
- backend: Spring Boot + SQL Server (.java, .xml, .yml)
|
|
1790
|
-
- frontend: React + Material-UI (.js, .jsx, .ts, .tsx, .css)
|
|
1791
|
-
- fullstack: Backend + Frontend with API consistency checks
|
|
1792
|
-
- database: SQL Server migrations and procedures (.sql)
|
|
1793
|
-
- ai: Node.js + Claude CLI integration (.js, .json, .md)
|
|
1794
|
-
- default: General-purpose mixed languages
|
|
1795
|
-
|
|
1796
|
-
Configuration (v2.2.0+):
|
|
1797
|
-
Create .claude/config.json in your project to customize:
|
|
1798
|
-
- Preset selection
|
|
1799
|
-
- Analysis settings (maxFileSize, maxFiles, timeout)
|
|
1800
|
-
- Commit message generation (autoKeyword, timeout)
|
|
1801
|
-
- Parallel execution (enabled, model, batchSize)
|
|
1802
|
-
- Template paths and output files
|
|
1803
|
-
- Debug mode
|
|
1804
|
-
|
|
1805
|
-
Example: .claude/config.json
|
|
1806
|
-
{
|
|
1807
|
-
"preset": "backend",
|
|
1808
|
-
"analysis": { "maxFiles": 30, "timeout": 180000 },
|
|
1809
|
-
"subagents": {
|
|
1810
|
-
"enabled": true, # Enable parallel execution
|
|
1811
|
-
"model": "haiku", # haiku (fast) | sonnet | opus
|
|
1812
|
-
"batchSize": 2 # Files per batch (1=fastest)
|
|
1813
|
-
},
|
|
1814
|
-
"system": { "debug": true }
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
Parallel Analysis (v2.2.0+):
|
|
1818
|
-
When analyzing 3+ files, parallel execution runs multiple Claude CLI
|
|
1819
|
-
processes simultaneously for faster analysis:
|
|
1820
|
-
- batchSize: 1 → Maximum speed (1 file per process)
|
|
1821
|
-
- batchSize: 2 → Balanced (2 files per process)
|
|
1822
|
-
- batchSize: 4+ → Fewer API calls but slower
|
|
1823
|
-
- Speed improvement: up to 4x faster with batchSize: 1
|
|
1824
|
-
|
|
1825
|
-
Debug Mode:
|
|
1826
|
-
Enable detailed logging for troubleshooting:
|
|
1827
|
-
- CLI: claude-hooks --debug true
|
|
1828
|
-
- Config: "system": { "debug": true } in .claude/config.json
|
|
1829
|
-
- Check status: claude-hooks --debug status
|
|
1830
|
-
|
|
1831
|
-
Customization:
|
|
1832
|
-
Override prompts by copying to .claude/:
|
|
1833
|
-
cp templates/COMMIT_MESSAGE.md .claude/
|
|
1834
|
-
cp templates/ANALYZE_DIFF.md .claude/
|
|
1835
|
-
cp templates/CLAUDE_PRE_COMMIT.md .claude/
|
|
1836
|
-
# Edit as needed - system uses .claude/ version if exists
|
|
1837
|
-
|
|
1838
|
-
More information: https://github.com/pablorovito/claude-git-hooks
|
|
1839
|
-
`);
|
|
1840
|
-
}
|
|
1841
|
-
|
|
1842
|
-
// Configuration Management Functions
|
|
1843
|
-
|
|
1844
|
-
/**
|
|
1845
|
-
* Updates a configuration value in .claude/config.json
|
|
1846
|
-
* Why: Centralized config update logic for all CLI commands
|
|
1847
|
-
*
|
|
1848
|
-
* @param {string} propertyPath - Dot notation path (e.g., 'preset', 'system.debug')
|
|
1849
|
-
* @param {any} value - Value to set
|
|
1850
|
-
* @param {Object} options - Optional settings
|
|
1851
|
-
* @param {Function} options.validator - Custom validation function, receives value, throws on invalid
|
|
1852
|
-
* @param {Function} options.successMessage - Function that receives value and returns success message
|
|
1853
|
-
*/
|
|
1854
|
-
async function updateConfig(propertyPath, value, options = {}) {
|
|
1855
|
-
const { validator, successMessage } = options;
|
|
1856
|
-
|
|
1857
|
-
try {
|
|
1858
|
-
// Validate value if validator provided
|
|
1859
|
-
if (validator) {
|
|
1860
|
-
await validator(value);
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
// Get repo root
|
|
1864
|
-
const repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();
|
|
1865
|
-
const configDir = path.join(repoRoot, '.claude');
|
|
1866
|
-
const configPath = path.join(configDir, 'config.json');
|
|
1867
|
-
|
|
1868
|
-
// Ensure .claude directory exists
|
|
1869
|
-
if (!fs.existsSync(configDir)) {
|
|
1870
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
1871
|
-
}
|
|
1872
|
-
|
|
1873
|
-
// Load existing config or create new
|
|
1874
|
-
let config = {};
|
|
1875
|
-
if (fs.existsSync(configPath)) {
|
|
1876
|
-
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
1877
|
-
}
|
|
1878
|
-
|
|
1879
|
-
// Set value at propertyPath (support dot notation like 'system.debug')
|
|
1880
|
-
const pathParts = propertyPath.split('.');
|
|
1881
|
-
let current = config;
|
|
1882
|
-
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
1883
|
-
const part = pathParts[i];
|
|
1884
|
-
if (!current[part] || typeof current[part] !== 'object') {
|
|
1885
|
-
current[part] = {};
|
|
1886
|
-
}
|
|
1887
|
-
current = current[part];
|
|
1888
|
-
}
|
|
1889
|
-
current[pathParts[pathParts.length - 1]] = value;
|
|
1890
|
-
|
|
1891
|
-
// Save config
|
|
1892
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
1893
|
-
|
|
1894
|
-
// Show success message
|
|
1895
|
-
const message = successMessage ? await successMessage(value) : 'Configuration updated';
|
|
1896
|
-
success(message);
|
|
1897
|
-
info(`Configuration saved to ${configPath}`);
|
|
1898
|
-
} catch (err) {
|
|
1899
|
-
error(`Failed to update configuration: ${err.message}`);
|
|
1900
|
-
process.exit(1);
|
|
1901
|
-
}
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
// Preset Management Functions
|
|
1905
|
-
|
|
1906
|
-
/**
|
|
1907
|
-
* Shows all available presets
|
|
1908
|
-
*/
|
|
1909
|
-
async function showPresets() {
|
|
1910
|
-
try {
|
|
1911
|
-
const presets = await listPresets();
|
|
1912
|
-
|
|
1913
|
-
if (presets.length === 0) {
|
|
1914
|
-
warning('No presets found');
|
|
1915
|
-
return;
|
|
1916
|
-
}
|
|
1917
|
-
|
|
1918
|
-
console.log('');
|
|
1919
|
-
info('Available presets:');
|
|
1920
|
-
console.log('');
|
|
1921
|
-
|
|
1922
|
-
presets.forEach(preset => {
|
|
1923
|
-
console.log(` ${colors.green}${preset.name}${colors.reset}`);
|
|
1924
|
-
console.log(` ${preset.displayName}`);
|
|
1925
|
-
console.log(` ${colors.blue}${preset.description}${colors.reset}`);
|
|
1926
|
-
console.log('');
|
|
1927
|
-
});
|
|
1928
|
-
|
|
1929
|
-
info('To set a preset: claude-hooks --set-preset <name>');
|
|
1930
|
-
info('To see current preset: claude-hooks preset current');
|
|
1931
|
-
console.log('');
|
|
1932
|
-
} catch (err) {
|
|
1933
|
-
error(`Failed to list presets: ${err.message}`);
|
|
1934
|
-
}
|
|
1935
|
-
}
|
|
1936
|
-
|
|
1937
|
-
/**
|
|
1938
|
-
* Sets the active preset
|
|
1939
|
-
* Why: Configures tech-stack specific analysis settings
|
|
1940
|
-
*/
|
|
1941
|
-
async function setPreset(presetName) {
|
|
1942
|
-
if (!presetName) {
|
|
1943
|
-
error('Please specify a preset name: claude-hooks --set-preset <name>');
|
|
1944
|
-
return;
|
|
1945
|
-
}
|
|
1946
|
-
|
|
1947
|
-
await updateConfig('preset', presetName, {
|
|
1948
|
-
validator: async (name) => {
|
|
1949
|
-
const presets = await listPresets();
|
|
1950
|
-
const preset = presets.find(p => p.name === name);
|
|
1951
|
-
if (!preset) {
|
|
1952
|
-
error(`Preset "${name}" not found`);
|
|
1953
|
-
info('Available presets:');
|
|
1954
|
-
presets.forEach(p => console.log(` - ${p.name}`));
|
|
1955
|
-
throw new Error(`Invalid preset: ${name}`);
|
|
1956
|
-
}
|
|
1957
|
-
return preset;
|
|
1958
|
-
},
|
|
1959
|
-
successMessage: async (name) => {
|
|
1960
|
-
const presets = await listPresets();
|
|
1961
|
-
const preset = presets.find(p => p.name === name);
|
|
1962
|
-
return `Preset '${preset.displayName}' activated`;
|
|
1963
|
-
}
|
|
1964
|
-
});
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
/**
|
|
1968
|
-
* Shows the current active preset
|
|
1969
|
-
*/
|
|
1970
|
-
async function currentPreset() {
|
|
1971
|
-
try {
|
|
1972
|
-
const config = await getConfig();
|
|
1973
|
-
const presetName = config.preset || 'default';
|
|
1974
|
-
|
|
1975
|
-
const presets = await listPresets();
|
|
1976
|
-
const preset = presets.find(p => p.name === presetName);
|
|
1977
|
-
|
|
1978
|
-
if (preset) {
|
|
1979
|
-
console.log('');
|
|
1980
|
-
success(`Current preset: ${preset.displayName} (${preset.name})`);
|
|
1981
|
-
console.log(` ${colors.blue}${preset.description}${colors.reset}`);
|
|
1982
|
-
console.log('');
|
|
1983
|
-
} else {
|
|
1984
|
-
warning(`Current preset "${presetName}" not found`);
|
|
1985
|
-
}
|
|
1986
|
-
} catch (err) {
|
|
1987
|
-
error(`Failed to get current preset: ${err.message}`);
|
|
1988
|
-
}
|
|
1989
|
-
}
|
|
1990
|
-
|
|
1991
|
-
// ============================================================================
|
|
1992
|
-
// DEPRECATED CODE SECTION - Will be removed in v3.0.0
|
|
1993
|
-
// ============================================================================
|
|
1994
|
-
// This section contains migration code for legacy configs (pre-v2.8.0)
|
|
1995
|
-
// Auto-executed during install when legacy config detected
|
|
1996
|
-
// Manual command: claude-hooks migrate-config
|
|
1997
|
-
// ============================================================================
|
|
1998
|
-
|
|
1999
|
-
/**
|
|
2000
|
-
* Extracts allowed settings from legacy config format
|
|
2001
|
-
* Shared by both autoMigrateConfig and migrateConfig
|
|
2002
|
-
*
|
|
2003
|
-
* @param {Object} rawConfig - Legacy format config
|
|
2004
|
-
* @returns {Object} Allowed overrides only
|
|
2005
|
-
*/
|
|
2006
|
-
function extractLegacySettings(rawConfig) {
|
|
2007
|
-
const allowedOverrides = {};
|
|
2008
|
-
|
|
2009
|
-
// GitHub PR config (fully allowed)
|
|
2010
|
-
if (rawConfig.github?.pr) {
|
|
2011
|
-
allowedOverrides.github = { pr: {} };
|
|
2012
|
-
if (rawConfig.github.pr.defaultBase !== undefined) {
|
|
2013
|
-
allowedOverrides.github.pr.defaultBase = rawConfig.github.pr.defaultBase;
|
|
2014
|
-
}
|
|
2015
|
-
if (rawConfig.github.pr.reviewers !== undefined) {
|
|
2016
|
-
allowedOverrides.github.pr.reviewers = rawConfig.github.pr.reviewers;
|
|
2017
|
-
}
|
|
2018
|
-
if (rawConfig.github.pr.labelRules !== undefined) {
|
|
2019
|
-
allowedOverrides.github.pr.labelRules = rawConfig.github.pr.labelRules;
|
|
2020
|
-
}
|
|
2021
|
-
}
|
|
2022
|
-
|
|
2023
|
-
// Subagent batchSize (allowed)
|
|
2024
|
-
if (rawConfig.subagents?.batchSize !== undefined) {
|
|
2025
|
-
allowedOverrides.subagents = { batchSize: rawConfig.subagents.batchSize };
|
|
2026
|
-
}
|
|
2027
|
-
|
|
2028
|
-
// Advanced params (preserved with warning in manual migration)
|
|
2029
|
-
if (rawConfig.analysis?.ignoreExtensions !== undefined) {
|
|
2030
|
-
if (!allowedOverrides.analysis) allowedOverrides.analysis = {};
|
|
2031
|
-
allowedOverrides.analysis.ignoreExtensions = rawConfig.analysis.ignoreExtensions;
|
|
2032
|
-
}
|
|
2033
|
-
|
|
2034
|
-
if (rawConfig.commitMessage?.taskIdPattern !== undefined) {
|
|
2035
|
-
if (!allowedOverrides.commitMessage) allowedOverrides.commitMessage = {};
|
|
2036
|
-
allowedOverrides.commitMessage.taskIdPattern = rawConfig.commitMessage.taskIdPattern;
|
|
2037
|
-
}
|
|
2038
|
-
|
|
2039
|
-
if (rawConfig.subagents?.model !== undefined) {
|
|
2040
|
-
if (!allowedOverrides.subagents) allowedOverrides.subagents = {};
|
|
2041
|
-
allowedOverrides.subagents.model = rawConfig.subagents.model;
|
|
2042
|
-
}
|
|
2043
|
-
|
|
2044
|
-
return allowedOverrides;
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2047
3
|
/**
|
|
2048
|
-
*
|
|
2049
|
-
*
|
|
4
|
+
* File: claude-hooks
|
|
5
|
+
* Purpose: Main CLI entry point - thin router to command modules
|
|
2050
6
|
*
|
|
2051
|
-
*
|
|
2052
|
-
*
|
|
7
|
+
* All command implementations are in lib/commands/
|
|
8
|
+
* This file only handles argument parsing and routing.
|
|
2053
9
|
*/
|
|
2054
|
-
async function autoMigrateConfig(newConfigPath, backupConfigPath) {
|
|
2055
|
-
try {
|
|
2056
|
-
const rawConfig = JSON.parse(fs.readFileSync(backupConfigPath, 'utf8'));
|
|
2057
|
-
const allowedOverrides = extractLegacySettings(rawConfig);
|
|
2058
10
|
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
}
|
|
11
|
+
import { error } from '../lib/commands/helpers.js';
|
|
12
|
+
|
|
13
|
+
// Import commands
|
|
14
|
+
import { runInstall } from '../lib/commands/install.js';
|
|
15
|
+
import { runEnable, runDisable, runStatus, runUninstall } from '../lib/commands/hooks.js';
|
|
16
|
+
import { runAnalyzeDiff } from '../lib/commands/analyze-diff.js';
|
|
17
|
+
import { runCreatePr } from '../lib/commands/create-pr.js';
|
|
18
|
+
import { runSetupGitHub } from '../lib/commands/setup-github.js';
|
|
19
|
+
import { runShowPresets, runSetPreset, runCurrentPreset } from '../lib/commands/presets.js';
|
|
20
|
+
import { runUpdate } from '../lib/commands/update.js';
|
|
21
|
+
import { runMigrateConfig } from '../lib/commands/migrate-config.js';
|
|
22
|
+
import { runSetDebug } from '../lib/commands/debug.js';
|
|
23
|
+
import { runShowTelemetry, runClearTelemetry } from '../lib/commands/telemetry-cmd.js';
|
|
24
|
+
import { runShowHelp, runShowVersion } from '../lib/commands/help.js';
|
|
2074
25
|
|
|
2075
26
|
/**
|
|
2076
|
-
*
|
|
2077
|
-
* Why: Simplifies configuration, reduces redundancy
|
|
2078
|
-
*
|
|
2079
|
-
* DEPRECATED: Will be removed in v3.0.0 (most users will have migrated by then)
|
|
27
|
+
* Main CLI router
|
|
2080
28
|
*/
|
|
2081
|
-
async function migrateConfig() {
|
|
2082
|
-
const claudeDir = '.claude';
|
|
2083
|
-
const configPath = path.join(claudeDir, 'config.json');
|
|
2084
|
-
|
|
2085
|
-
if (!fs.existsSync(configPath)) {
|
|
2086
|
-
info('ℹ️ No config file found. Nothing to migrate.');
|
|
2087
|
-
console.log('\n💡 To create a new config:');
|
|
2088
|
-
console.log(' 1. Run: claude-hooks install --force');
|
|
2089
|
-
console.log(' 2. Or copy from: .claude/config_example/config.example.json');
|
|
2090
|
-
return;
|
|
2091
|
-
}
|
|
2092
|
-
|
|
2093
|
-
try {
|
|
2094
|
-
const rawConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
2095
|
-
|
|
2096
|
-
// Check if already in v2.8.0 format
|
|
2097
|
-
if (rawConfig.version === '2.8.0') {
|
|
2098
|
-
success('✅ Config is already in v2.8.0 format.');
|
|
2099
|
-
return;
|
|
2100
|
-
}
|
|
2101
|
-
|
|
2102
|
-
info('📦 Starting config migration to v2.8.0...');
|
|
2103
|
-
|
|
2104
|
-
// Create backup in config_old/
|
|
2105
|
-
const configOldDir = path.join(claudeDir, 'config_old');
|
|
2106
|
-
if (!fs.existsSync(configOldDir)) {
|
|
2107
|
-
fs.mkdirSync(configOldDir, { recursive: true });
|
|
2108
|
-
}
|
|
2109
|
-
const backupPath = path.join(configOldDir, `config.json.${Date.now()}`);
|
|
2110
|
-
fs.copyFileSync(configPath, backupPath);
|
|
2111
|
-
success(`Backup created: ${backupPath}`);
|
|
2112
|
-
|
|
2113
|
-
// Extract only allowed parameters
|
|
2114
|
-
const allowedOverrides = extractLegacySettings(rawConfig);
|
|
2115
|
-
|
|
2116
|
-
// Check for advanced params
|
|
2117
|
-
const hasAdvancedParams = allowedOverrides.analysis?.ignoreExtensions ||
|
|
2118
|
-
allowedOverrides.commitMessage?.taskIdPattern ||
|
|
2119
|
-
allowedOverrides.subagents?.model;
|
|
2120
|
-
|
|
2121
|
-
// Build new config
|
|
2122
|
-
const newConfig = {
|
|
2123
|
-
version: '2.8.0',
|
|
2124
|
-
preset: rawConfig.preset || 'default'
|
|
2125
|
-
};
|
|
2126
|
-
|
|
2127
|
-
// Only add overrides if there are any
|
|
2128
|
-
if (Object.keys(allowedOverrides).length > 0) {
|
|
2129
|
-
newConfig.overrides = allowedOverrides;
|
|
2130
|
-
}
|
|
2131
|
-
|
|
2132
|
-
// Show diff
|
|
2133
|
-
console.log('\n📝 Migration preview:');
|
|
2134
|
-
console.log(` Old format: ${Object.keys(rawConfig).length} top-level keys`);
|
|
2135
|
-
console.log(` New format: ${Object.keys(newConfig).length} top-level keys`);
|
|
2136
|
-
if (Object.keys(allowedOverrides).length > 0) {
|
|
2137
|
-
console.log(` Preserved: ${Object.keys(allowedOverrides).length} override sections`);
|
|
2138
|
-
}
|
|
2139
|
-
|
|
2140
|
-
// Write new config
|
|
2141
|
-
fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 4));
|
|
2142
|
-
success('✅ Config migrated to v2.8.0 successfully!');
|
|
2143
|
-
|
|
2144
|
-
if (hasAdvancedParams) {
|
|
2145
|
-
warning('⚠️ Advanced parameters detected and preserved');
|
|
2146
|
-
info('📖 See .claude/config.advanced.example.json for documentation');
|
|
2147
|
-
}
|
|
2148
|
-
|
|
2149
|
-
console.log(`\n✨ New config:`);
|
|
2150
|
-
console.log(JSON.stringify(newConfig, null, 2));
|
|
2151
|
-
console.log(`\n💾 Old config backed up to: ${backupPath}`);
|
|
2152
|
-
console.log('\n💡 Many parameters are now hardcoded with sensible defaults');
|
|
2153
|
-
console.log(' See CHANGELOG.md for full list of changes');
|
|
2154
|
-
|
|
2155
|
-
} catch (error) {
|
|
2156
|
-
error(`Failed to migrate config: ${error.message}`);
|
|
2157
|
-
console.log('\n💡 Manual migration:');
|
|
2158
|
-
console.log(' 1. Backup your current config');
|
|
2159
|
-
console.log(' 2. See .claude/config.example.json for new format');
|
|
2160
|
-
console.log(' 3. Copy minimal example and customize');
|
|
2161
|
-
}
|
|
2162
|
-
}
|
|
2163
|
-
|
|
2164
|
-
/**
|
|
2165
|
-
* Sets debug mode
|
|
2166
|
-
* Why: Enables detailed logging for troubleshooting
|
|
2167
|
-
*/
|
|
2168
|
-
async function setDebug(value) {
|
|
2169
|
-
if (!value) {
|
|
2170
|
-
error('Please specify a value: claude-hooks --debug <true|false|status>');
|
|
2171
|
-
return;
|
|
2172
|
-
}
|
|
2173
|
-
|
|
2174
|
-
const normalizedValue = value.toLowerCase();
|
|
2175
|
-
|
|
2176
|
-
// Handle status check
|
|
2177
|
-
if (normalizedValue === 'status') {
|
|
2178
|
-
try {
|
|
2179
|
-
const config = await getConfig();
|
|
2180
|
-
const isEnabled = config.system.debug || false;
|
|
2181
|
-
console.log('');
|
|
2182
|
-
info(`Debug mode: ${isEnabled ? colors.green + 'enabled' + colors.reset : colors.red + 'disabled' + colors.reset}`);
|
|
2183
|
-
console.log('');
|
|
2184
|
-
} catch (err) {
|
|
2185
|
-
error(`Failed to check debug status: ${err.message}`);
|
|
2186
|
-
}
|
|
2187
|
-
return;
|
|
2188
|
-
}
|
|
2189
|
-
|
|
2190
|
-
// Validate and convert to boolean
|
|
2191
|
-
if (normalizedValue !== 'true' && normalizedValue !== 'false') {
|
|
2192
|
-
error('Invalid value. Use: true, false, or status');
|
|
2193
|
-
return;
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
|
-
const debugValue = normalizedValue === 'true';
|
|
2197
|
-
|
|
2198
|
-
await updateConfig('system.debug', debugValue, {
|
|
2199
|
-
successMessage: (val) => `Debug mode ${val ? 'enabled' : 'disabled'}`
|
|
2200
|
-
});
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2203
|
-
// Main
|
|
2204
|
-
/**
|
|
2205
|
-
* Show telemetry statistics
|
|
2206
|
-
* Why: Help users understand JSON parsing patterns and batch performance
|
|
2207
|
-
*/
|
|
2208
|
-
async function showTelemetry() {
|
|
2209
|
-
await showTelemetryStats();
|
|
2210
|
-
}
|
|
2211
|
-
|
|
2212
|
-
/**
|
|
2213
|
-
* Clear telemetry data
|
|
2214
|
-
* Why: Allow users to reset telemetry
|
|
2215
|
-
*/
|
|
2216
|
-
async function clearTelemetry() {
|
|
2217
|
-
const config = await getConfig();
|
|
2218
|
-
|
|
2219
|
-
if (!config.system?.telemetry && config.system?.telemetry !== undefined) {
|
|
2220
|
-
console.log('\n⚠️ Telemetry is currently disabled.\n');
|
|
2221
|
-
console.log('To re-enable (default), remove or set to true in .claude/config.json:');
|
|
2222
|
-
console.log('{');
|
|
2223
|
-
console.log(' "system": {');
|
|
2224
|
-
console.log(' "telemetry": true');
|
|
2225
|
-
console.log(' }');
|
|
2226
|
-
console.log('}\n');
|
|
2227
|
-
return;
|
|
2228
|
-
}
|
|
2229
|
-
|
|
2230
|
-
const confirmed = await promptConfirmation('Are you sure you want to clear all telemetry data?');
|
|
2231
|
-
if (confirmed) {
|
|
2232
|
-
await clearTelemetryData();
|
|
2233
|
-
success('Telemetry data cleared successfully');
|
|
2234
|
-
} else {
|
|
2235
|
-
info('Telemetry data was not cleared');
|
|
2236
|
-
}
|
|
2237
|
-
}
|
|
2238
|
-
|
|
2239
29
|
async function main() {
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
error(`Unknown command: ${command}`);
|
|
2317
|
-
showHelp();
|
|
2318
|
-
}
|
|
30
|
+
const args = process.argv.slice(2);
|
|
31
|
+
const command = args[0];
|
|
32
|
+
|
|
33
|
+
switch (command) {
|
|
34
|
+
case 'install':
|
|
35
|
+
await runInstall(args.slice(1));
|
|
36
|
+
break;
|
|
37
|
+
case 'update':
|
|
38
|
+
await runUpdate();
|
|
39
|
+
break;
|
|
40
|
+
case 'uninstall':
|
|
41
|
+
runUninstall();
|
|
42
|
+
break;
|
|
43
|
+
case 'enable':
|
|
44
|
+
runEnable(args[1]);
|
|
45
|
+
break;
|
|
46
|
+
case 'disable':
|
|
47
|
+
runDisable(args[1]);
|
|
48
|
+
break;
|
|
49
|
+
case 'status':
|
|
50
|
+
runStatus();
|
|
51
|
+
break;
|
|
52
|
+
case 'analyze-diff':
|
|
53
|
+
await runAnalyzeDiff(args.slice(1));
|
|
54
|
+
break;
|
|
55
|
+
case 'create-pr':
|
|
56
|
+
await runCreatePr(args.slice(1));
|
|
57
|
+
break;
|
|
58
|
+
case 'setup-github':
|
|
59
|
+
await runSetupGitHub();
|
|
60
|
+
break;
|
|
61
|
+
case 'presets':
|
|
62
|
+
await runShowPresets();
|
|
63
|
+
break;
|
|
64
|
+
case '--set-preset':
|
|
65
|
+
await runSetPreset(args[1]);
|
|
66
|
+
break;
|
|
67
|
+
case 'preset':
|
|
68
|
+
// Handle subcommands: preset current
|
|
69
|
+
if (args[1] === 'current') {
|
|
70
|
+
await runCurrentPreset();
|
|
71
|
+
} else {
|
|
72
|
+
error(`Unknown preset subcommand: ${args[1]}`);
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
case 'migrate-config':
|
|
76
|
+
await runMigrateConfig();
|
|
77
|
+
break;
|
|
78
|
+
case 'telemetry':
|
|
79
|
+
// Handle subcommands: telemetry show, telemetry clear
|
|
80
|
+
if (args[1] === 'show' || args[1] === undefined) {
|
|
81
|
+
await runShowTelemetry();
|
|
82
|
+
} else if (args[1] === 'clear') {
|
|
83
|
+
await runClearTelemetry();
|
|
84
|
+
} else {
|
|
85
|
+
error(`Unknown telemetry subcommand: ${args[1]}`);
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
case '--debug':
|
|
89
|
+
await runSetDebug(args[1]);
|
|
90
|
+
break;
|
|
91
|
+
case '--version':
|
|
92
|
+
case '-v':
|
|
93
|
+
case 'version':
|
|
94
|
+
runShowVersion();
|
|
95
|
+
break;
|
|
96
|
+
case 'help':
|
|
97
|
+
case '--help':
|
|
98
|
+
case '-h':
|
|
99
|
+
case undefined:
|
|
100
|
+
runShowHelp();
|
|
101
|
+
break;
|
|
102
|
+
default:
|
|
103
|
+
error(`Unknown command: ${command}`);
|
|
104
|
+
runShowHelp();
|
|
105
|
+
}
|
|
2319
106
|
}
|
|
2320
107
|
|
|
2321
108
|
// Execute main
|
|
2322
109
|
main().catch(err => {
|
|
2323
|
-
|
|
2324
|
-
});
|
|
110
|
+
error(`Unexpected error: ${err.message}`);
|
|
111
|
+
});
|