claude-git-hooks 2.8.0 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,147 @@
1
+ /**
2
+ * File: help.js
3
+ * Purpose: Help and version display commands
4
+ */
5
+
6
+ import { getPackageJson } from './helpers.js';
7
+
8
+ /**
9
+ * Show version command
10
+ * Why: Reusable function to display current version from package.json
11
+ */
12
+ export function runShowVersion() {
13
+ const pkg = getPackageJson();
14
+ console.log(`${pkg.name} v${pkg.version}`);
15
+ }
16
+
17
+ /**
18
+ * Show help command
19
+ */
20
+ export function runShowHelp() {
21
+ console.log(`
22
+ Claude Git Hooks - Code analysis and automatic messages with Claude CLI
23
+
24
+ Usage: claude-hooks <command> [options]
25
+
26
+ Commands:
27
+ install [options] Install hooks in the current repository
28
+ --force Reinstall even if they already exist
29
+ --skip-auth Skip Claude authentication verification
30
+ update Update to the latest available version
31
+ uninstall Uninstall hooks from the repository
32
+ enable [hook] Enable hooks (all or one specific)
33
+ disable [hook] Disable hooks (all or one specific)
34
+ status Show the status of hooks
35
+ analyze-diff [base] Analyze differences between branches and generate PR info
36
+ create-pr [base] Create pull request with auto-generated metadata and reviewers
37
+ setup-github Setup GitHub login (required for create-pr)
38
+ presets List all available presets
39
+ --set-preset <name> Set the active preset
40
+ preset current Show the current active preset
41
+ telemetry [action] Telemetry management (show or clear)
42
+ --debug <value> Set debug mode (true, false, or status)
43
+ --version, -v Show the current version
44
+ help Show this help
45
+
46
+ Available hooks:
47
+ pre-commit Code analysis before commit
48
+ prepare-commit-msg Automatic message generation
49
+
50
+ Examples:
51
+ claude-hooks install # Install all hooks
52
+ claude-hooks install --skip-auth # Install without verifying authentication
53
+ claude-hooks update # Update to the latest version
54
+ claude-hooks disable pre-commit # Disable only pre-commit
55
+ claude-hooks enable # Enable all hooks
56
+ claude-hooks status # View current status
57
+ claude-hooks analyze-diff main # Analyze differences with main
58
+ claude-hooks setup-github # Configure GitHub token for PR creation
59
+ claude-hooks create-pr develop # Create PR targeting develop branch
60
+ claude-hooks presets # List available presets
61
+ claude-hooks --set-preset backend # Set backend preset
62
+ claude-hooks preset current # Show current preset
63
+ claude-hooks telemetry show # Show telemetry statistics
64
+ claude-hooks telemetry clear # Clear telemetry data
65
+ claude-hooks --debug true # Enable debug mode
66
+ claude-hooks --debug status # Check debug status
67
+
68
+ Commit use cases:
69
+ git commit -m "message" # Manual message + blocking analysis
70
+ git commit -m "auto" # Automatic message + blocking analysis
71
+ git commit --no-verify -m "auto" # Automatic message without analysis
72
+ git commit --no-verify -m "msg" # Manual message without analysis
73
+
74
+ Analyze-diff use case:
75
+ claude-hooks analyze-diff main # Analyze changes vs main and generate:
76
+ → PR Title: "feat: add user authentication module"
77
+ → PR Description: "## Summary\n- Added JWT authentication..."
78
+ → Suggested branch: "feature/user-authentication"
79
+
80
+ Create-pr use case (v2.5.0+):
81
+ claude-hooks create-pr develop # Create PR targeting develop:
82
+ → Validates GitHub token
83
+ → Extracts task-id from branch (IX-123, #456, LIN-123)
84
+ → Analyzes diff and generates PR metadata with Claude
85
+ → Creates PR directly via GitHub API (Octokit)
86
+ → Adds labels based on preset
87
+ → Returns PR URL
88
+
89
+ Token configuration:
90
+ → .claude/settings.local.json (recommended, gitignored)
91
+ → GITHUB_TOKEN environment variable
92
+ → Claude Desktop config (auto-detected)
93
+
94
+ Presets (v2.3.0+):
95
+ Built-in tech-stack specific configurations:
96
+ - backend: Spring Boot + SQL Server (.java, .xml, .yml)
97
+ - frontend: React + Material-UI (.js, .jsx, .ts, .tsx, .css)
98
+ - fullstack: Backend + Frontend with API consistency checks
99
+ - database: SQL Server migrations and procedures (.sql)
100
+ - ai: Node.js + Claude CLI integration (.js, .json, .md)
101
+ - default: General-purpose mixed languages
102
+
103
+ Configuration (v2.2.0+):
104
+ Create .claude/config.json in your project to customize:
105
+ - Preset selection
106
+ - Analysis settings (maxFileSize, maxFiles, timeout)
107
+ - Commit message generation (autoKeyword, timeout)
108
+ - Parallel execution (enabled, model, batchSize)
109
+ - Template paths and output files
110
+ - Debug mode
111
+
112
+ Example: .claude/config.json
113
+ {
114
+ "preset": "backend",
115
+ "analysis": { "maxFiles": 30, "timeout": 180000 },
116
+ "subagents": {
117
+ "enabled": true, // Enable parallel execution
118
+ "model": "haiku", // haiku (fast) | sonnet | opus
119
+ "batchSize": 2 // Files per batch (1=fastest)
120
+ },
121
+ "system": { "debug": true }
122
+ }
123
+
124
+ Parallel Analysis (v2.2.0+):
125
+ When analyzing 3+ files, parallel execution runs multiple Claude CLI
126
+ processes simultaneously for faster analysis:
127
+ - batchSize: 1 → Maximum speed (1 file per process)
128
+ - batchSize: 2 → Balanced (2 files per process)
129
+ - batchSize: 4+ → Fewer API calls but slower
130
+ - Speed improvement: up to 4x faster with batchSize: 1
131
+
132
+ Debug Mode:
133
+ Enable detailed logging for troubleshooting:
134
+ - CLI: claude-hooks --debug true
135
+ - Config: "system": { "debug": true } in .claude/config.json
136
+ - Check status: claude-hooks --debug status
137
+
138
+ Customization:
139
+ Override prompts by copying to .claude/:
140
+ cp templates/COMMIT_MESSAGE.md .claude/
141
+ cp templates/ANALYZE_DIFF.md .claude/
142
+ cp templates/CLAUDE_PRE_COMMIT.md .claude/
143
+ # Edit as needed - system uses .claude/ version if exists
144
+
145
+ More information: https://github.com/pablorovito/claude-git-hooks
146
+ `);
147
+ }
@@ -0,0 +1,389 @@
1
+ /**
2
+ * File: helpers.js
3
+ * Purpose: Shared utilities for CLI commands
4
+ *
5
+ * Exports:
6
+ * - Terminal colors and output functions
7
+ * - Git repository checks
8
+ * - Platform detection
9
+ * - Package.json utilities
10
+ * - Config management
11
+ * - Entertainment (spinner with jokes)
12
+ */
13
+
14
+ import { execSync } from 'child_process';
15
+ import fs from 'fs';
16
+ import path from 'path';
17
+ import os from 'os';
18
+ import https from 'https';
19
+ import { fileURLToPath } from 'url';
20
+ import { dirname } from 'path';
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
+ // Colors for terminal output
27
+ export const colors = {
28
+ reset: '\x1b[0m',
29
+ red: '\x1b[31m',
30
+ green: '\x1b[32m',
31
+ yellow: '\x1b[33m',
32
+ blue: '\x1b[34m'
33
+ };
34
+
35
+ export function log(message, color = 'reset') {
36
+ console.log(`${colors[color]}${message}${colors.reset}`);
37
+ }
38
+
39
+ export function error(message) {
40
+ console.error(`${colors.red}❌ ${message}${colors.reset}`);
41
+ process.exit(1);
42
+ }
43
+
44
+ export function success(message) {
45
+ log(`✅ ${message}`, 'green');
46
+ }
47
+
48
+ export function info(message) {
49
+ log(`ℹ️ ${message}`, 'blue');
50
+ }
51
+
52
+ export function warning(message) {
53
+ log(`⚠️ ${message}`, 'yellow');
54
+ }
55
+
56
+ /**
57
+ * Check if we are in a git repository (including worktrees created in PowerShell)
58
+ * @returns {boolean}
59
+ */
60
+ export function checkGitRepo() {
61
+ try {
62
+ execSync('git rev-parse --git-dir', { stdio: 'ignore' });
63
+ return true;
64
+ } catch (e) {
65
+ // Try to detect worktree created in PowerShell
66
+ try {
67
+ if (fs.existsSync('.git')) {
68
+ const gitContent = fs.readFileSync('.git', 'utf8').trim();
69
+ // Check if it's a worktree pointer (gitdir: ...)
70
+ if (gitContent.startsWith('gitdir:')) {
71
+ let gitdir = gitContent.substring(8).trim();
72
+ // Convert Windows path to WSL if needed (C:\ -> /mnt/c/)
73
+ if (/^[A-Za-z]:/.test(gitdir)) {
74
+ gitdir = gitdir.replace(/^([A-Za-z]):/, (_, drive) => `/mnt/${drive.toLowerCase()}`);
75
+ gitdir = gitdir.replace(/\\/g, '/');
76
+ }
77
+ // Verify the gitdir exists
78
+ if (fs.existsSync(gitdir)) {
79
+ return true;
80
+ }
81
+ }
82
+ }
83
+ } catch (worktreeError) {
84
+ // Ignore worktree detection errors
85
+ }
86
+ return false;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Get the templates path relative to package root
92
+ * @returns {string}
93
+ */
94
+ export function getTemplatesPath() {
95
+ return path.join(__dirname, '..', '..', 'templates');
96
+ }
97
+
98
+ /**
99
+ * Detect if running on Windows
100
+ * Why: Need to use 'wsl claude' instead of 'claude' on Windows
101
+ * @returns {boolean}
102
+ */
103
+ export function isWindows() {
104
+ return os.platform() === 'win32' || process.env.OS === 'Windows_NT';
105
+ }
106
+
107
+ /**
108
+ * Get Claude command based on platform
109
+ * Why: On Windows, try native Claude first, then WSL as fallback
110
+ * @returns {string}
111
+ */
112
+ export function getClaudeCommand() {
113
+ if (isWindows()) {
114
+ // Try native Windows Claude first
115
+ try {
116
+ execSync('claude --version', { stdio: 'ignore', timeout: 3000 });
117
+ return 'claude';
118
+ } catch (e) {
119
+ // Fallback to WSL
120
+ return 'wsl claude';
121
+ }
122
+ }
123
+ return 'claude';
124
+ }
125
+
126
+ /**
127
+ * Helper to read package.json
128
+ * Why: ES6 modules can't use require() for JSON files
129
+ * @returns {Object}
130
+ */
131
+ export function getPackageJson() {
132
+ const packagePath = path.join(__dirname, '..', '..', 'package.json');
133
+ return JSON.parse(fs.readFileSync(packagePath, 'utf8'));
134
+ }
135
+
136
+ /**
137
+ * Cross-platform version comparison (semver)
138
+ * Why: Pure JavaScript, no bash dependency
139
+ * @param {string} v1 - First version
140
+ * @param {string} v2 - Second version
141
+ * @returns {number} 0 if equal, 1 if v1 > v2, -1 if v1 < v2
142
+ */
143
+ export function compareVersions(v1, v2) {
144
+ if (v1 === v2) return 0;
145
+
146
+ const v1Parts = v1.split('.').map(Number);
147
+ const v2Parts = v2.split('.').map(Number);
148
+
149
+ for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
150
+ const v1Part = v1Parts[i] || 0;
151
+ const v2Part = v2Parts[i] || 0;
152
+
153
+ if (v1Part > v2Part) return 1;
154
+ if (v1Part < v2Part) return -1;
155
+ }
156
+
157
+ return 0;
158
+ }
159
+
160
+ /**
161
+ * Updates a configuration value in .claude/config.json
162
+ * Why: Centralized config update logic for all CLI commands
163
+ *
164
+ * @param {string} propertyPath - Dot notation path (e.g., 'preset', 'system.debug')
165
+ * @param {any} value - Value to set
166
+ * @param {Object} options - Optional settings
167
+ * @param {Function} options.validator - Custom validation function, receives value, throws on invalid
168
+ * @param {Function} options.successMessage - Function that receives value and returns success message
169
+ */
170
+ export async function updateConfig(propertyPath, value, options = {}) {
171
+ const { validator, successMessage } = options;
172
+
173
+ try {
174
+ // Validate value if validator provided
175
+ if (validator) {
176
+ await validator(value);
177
+ }
178
+
179
+ // Get repo root
180
+ const repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();
181
+ const configDir = path.join(repoRoot, '.claude');
182
+ const configPath = path.join(configDir, 'config.json');
183
+
184
+ // Ensure .claude directory exists
185
+ if (!fs.existsSync(configDir)) {
186
+ fs.mkdirSync(configDir, { recursive: true });
187
+ }
188
+
189
+ // Load existing config or create new
190
+ let config = {};
191
+ if (fs.existsSync(configPath)) {
192
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
193
+ }
194
+
195
+ // Set value at propertyPath (support dot notation like 'system.debug')
196
+ const pathParts = propertyPath.split('.');
197
+ let current = config;
198
+ for (let i = 0; i < pathParts.length - 1; i++) {
199
+ const part = pathParts[i];
200
+ if (!current[part] || typeof current[part] !== 'object') {
201
+ current[part] = {};
202
+ }
203
+ current = current[part];
204
+ }
205
+ current[pathParts[pathParts.length - 1]] = value;
206
+
207
+ // Save config
208
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
209
+
210
+ // Show success message
211
+ const message = successMessage ? await successMessage(value) : 'Configuration updated';
212
+ success(message);
213
+ info(`Configuration saved to ${configPath}`);
214
+ } catch (err) {
215
+ error(`Failed to update configuration: ${err.message}`);
216
+ process.exit(1);
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Function to get the latest version from NPM
222
+ * @param {string} packageName
223
+ * @returns {Promise<string>}
224
+ */
225
+ export function getLatestVersion(packageName) {
226
+ return new Promise((resolve, reject) => {
227
+ // Use the main NPM API, not /latest
228
+ https.get(`https://registry.npmjs.org/${packageName}`, (res) => {
229
+ let data = '';
230
+ res.on('data', chunk => data += chunk);
231
+ res.on('end', () => {
232
+ try {
233
+ const json = JSON.parse(data);
234
+ // Get the version from the 'latest' tag
235
+ if (json['dist-tags'] && json['dist-tags'].latest) {
236
+ resolve(json['dist-tags'].latest);
237
+ } else {
238
+ reject(new Error('Could not get the version'));
239
+ }
240
+ } catch (e) {
241
+ // If it fails, try with npm view
242
+ try {
243
+ const version = execSync(`npm view ${packageName} version`, { encoding: 'utf8' }).trim();
244
+ resolve(version);
245
+ } catch (npmError) {
246
+ reject(e);
247
+ }
248
+ }
249
+ });
250
+ }).on('error', reject);
251
+ });
252
+ }
253
+
254
+ /**
255
+ * Entertainment system - spinner with jokes for long operations
256
+ */
257
+ export class Entertainment {
258
+ static jokes = [
259
+ "Why do programmers prefer dark mode? Because light attracts bugs!",
260
+ "A QA engineer walks into a bar. Orders 1 beer. Orders 0 beers. Orders -1 beers.",
261
+ "What's a pirate's favorite programming language? R!",
262
+ "There are 10 types of people: those who understand binary and those who don't.",
263
+ "Why do programmers confuse Halloween with Christmas? Because Oct 31 = Dec 25",
264
+ "What does one bit say to another? See you on the bus!",
265
+ "Why don't Java and C++ get along? Because they have different views on pointers.",
266
+ "My code doesn't have bugs, just undocumented features."
267
+ ];
268
+
269
+ static async getJoke() {
270
+ return new Promise((resolve) => {
271
+ // Try to get joke from API
272
+ const req = https.get('https://icanhazdadjoke.com/', {
273
+ headers: { 'Accept': 'text/plain' },
274
+ timeout: 3000
275
+ }, (res) => {
276
+ let data = '';
277
+ res.on('data', chunk => data += chunk);
278
+ res.on('end', () => resolve(data.trim()));
279
+ });
280
+
281
+ req.on('error', () => {
282
+ // If it fails, use local joke
283
+ const randomJoke = this.jokes[Math.floor(Math.random() * this.jokes.length)];
284
+ resolve(randomJoke);
285
+ });
286
+
287
+ req.on('timeout', () => {
288
+ req.abort();
289
+ const randomJoke = this.jokes[Math.floor(Math.random() * this.jokes.length)];
290
+ resolve(randomJoke);
291
+ });
292
+ });
293
+ }
294
+
295
+ static async showSpinner(promise, message = 'Processing') {
296
+ const spinners = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
297
+ let spinnerIndex = 0;
298
+ let jokeCountdown = 10;
299
+ let currentJoke = this.jokes[Math.floor(Math.random() * this.jokes.length)];
300
+ let isFinished = false;
301
+
302
+ // Get first joke from API without blocking
303
+ this.getJoke().then(joke => {
304
+ if (!isFinished) currentJoke = joke;
305
+ }).catch(() => { }); // If it fails, keep the local one
306
+
307
+ // Hide cursor
308
+ process.stdout.write('\x1B[?25l');
309
+
310
+ // Reserve space for the 3 lines
311
+ process.stdout.write('\n\n\n');
312
+
313
+ const interval = setInterval(() => {
314
+ if (isFinished) {
315
+ clearInterval(interval);
316
+ return;
317
+ }
318
+
319
+ spinnerIndex++;
320
+
321
+ // Update countdown every second (10 iterations of 100ms)
322
+ if (spinnerIndex % 10 === 0) {
323
+ jokeCountdown--;
324
+
325
+ // Refresh joke every 10 seconds
326
+ if (jokeCountdown <= 0) {
327
+ this.getJoke().then(joke => {
328
+ if (!isFinished) currentJoke = joke;
329
+ }).catch(() => {
330
+ if (!isFinished) {
331
+ currentJoke = this.jokes[Math.floor(Math.random() * this.jokes.length)];
332
+ }
333
+ });
334
+ jokeCountdown = 10;
335
+ }
336
+ }
337
+
338
+ // Always go back exactly 3 lines up
339
+ process.stdout.write('\x1B[3A');
340
+
341
+ // Render the 3 lines from the beginning
342
+ const spinner = spinners[spinnerIndex % spinners.length];
343
+
344
+ // Line 1: Spinner
345
+ process.stdout.write('\r\x1B[2K' + `${colors.yellow}${spinner} ${message}${colors.reset}\n`);
346
+
347
+ // Line 2: Joke
348
+ process.stdout.write('\r\x1B[2K' + `${colors.green}🎭 ${currentJoke}${colors.reset}\n`);
349
+
350
+ // Line 3: Countdown
351
+ process.stdout.write('\r\x1B[2K' + `${colors.yellow}⏱️ Next joke in: ${jokeCountdown}s${colors.reset}\n`);
352
+ }, 100);
353
+
354
+ try {
355
+ const result = await promise;
356
+ isFinished = true;
357
+ clearInterval(interval);
358
+
359
+ // Clean exactly 3 lines completely
360
+ process.stdout.write('\x1B[3A'); // Go up 3 lines
361
+ process.stdout.write('\r\x1B[2K'); // Clean line 1
362
+ process.stdout.write('\n\r\x1B[2K'); // Go down and clean line 2
363
+ process.stdout.write('\n\r\x1B[2K'); // Go down and clean line 3
364
+ process.stdout.write('\x1B[2A'); // Go up 2 lines to end up on the first
365
+ process.stdout.write('\r'); // Go to beginning of line
366
+
367
+ // Show cursor
368
+ process.stdout.write('\x1B[?25h');
369
+
370
+ return result;
371
+ } catch (error) {
372
+ isFinished = true;
373
+ clearInterval(interval);
374
+
375
+ // Clean exactly 3 lines completely
376
+ process.stdout.write('\x1B[3A'); // Go up 3 lines
377
+ process.stdout.write('\r\x1B[2K'); // Clean line 1
378
+ process.stdout.write('\n\r\x1B[2K'); // Go down and clean line 2
379
+ process.stdout.write('\n\r\x1B[2K'); // Go down and clean line 3
380
+ process.stdout.write('\x1B[2A'); // Go up 2 lines to end up on the first
381
+ process.stdout.write('\r'); // Go to beginning of line
382
+
383
+ // Show cursor
384
+ process.stdout.write('\x1B[?25h');
385
+
386
+ throw error;
387
+ }
388
+ }
389
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * File: hooks.js
3
+ * Purpose: Hook management commands (enable, disable, status, uninstall)
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import {
9
+ error,
10
+ success,
11
+ info,
12
+ warning,
13
+ checkGitRepo
14
+ } from './helpers.js';
15
+
16
+ /**
17
+ * Enable command
18
+ * @param {string} hookName - Optional specific hook to enable
19
+ */
20
+ export function runEnable(hookName) {
21
+ if (!checkGitRepo()) {
22
+ error('You are not in a Git repository.');
23
+ }
24
+
25
+ const hooks = hookName ? [hookName] : ['pre-commit', 'prepare-commit-msg'];
26
+
27
+ hooks.forEach(hook => {
28
+ const disabledPath = `.git/hooks/${hook}.disabled`;
29
+ const enabledPath = `.git/hooks/${hook}`;
30
+
31
+ if (fs.existsSync(disabledPath)) {
32
+ fs.renameSync(disabledPath, enabledPath);
33
+ success(`${hook} enabled`);
34
+ } else if (fs.existsSync(enabledPath)) {
35
+ info(`${hook} is already enabled`);
36
+ } else {
37
+ warning(`${hook} not found`);
38
+ }
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Disable command
44
+ * @param {string} hookName - Optional specific hook to disable
45
+ */
46
+ export function runDisable(hookName) {
47
+ if (!checkGitRepo()) {
48
+ error('You are not in a Git repository.');
49
+ }
50
+
51
+ const hooks = hookName ? [hookName] : ['pre-commit', 'prepare-commit-msg'];
52
+
53
+ hooks.forEach(hook => {
54
+ const enabledPath = `.git/hooks/${hook}`;
55
+ const disabledPath = `.git/hooks/${hook}.disabled`;
56
+
57
+ if (fs.existsSync(enabledPath)) {
58
+ fs.renameSync(enabledPath, disabledPath);
59
+ success(`${hook} disabled`);
60
+ } else if (fs.existsSync(disabledPath)) {
61
+ info(`${hook} is already disabled`);
62
+ } else {
63
+ warning(`${hook} not found`);
64
+ }
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Status command
70
+ */
71
+ export function runStatus() {
72
+ if (!checkGitRepo()) {
73
+ error('You are not in a Git repository.');
74
+ }
75
+
76
+ info('Claude Git Hooks status:\n');
77
+
78
+ const hooks = ['pre-commit', 'prepare-commit-msg'];
79
+ hooks.forEach(hook => {
80
+ const enabledPath = `.git/hooks/${hook}`;
81
+ const disabledPath = `.git/hooks/${hook}.disabled`;
82
+
83
+ if (fs.existsSync(enabledPath)) {
84
+ success(`${hook}: enabled`);
85
+ } else if (fs.existsSync(disabledPath)) {
86
+ warning(`${hook}: disabled`);
87
+ } else {
88
+ error(`${hook}: not installed`);
89
+ }
90
+ });
91
+
92
+ // Check guidelines files
93
+ console.log('\nGuidelines files:');
94
+ const guidelines = ['CLAUDE_PRE_COMMIT.md'];
95
+ guidelines.forEach(guideline => {
96
+ const promptsPath = path.join('.claude', 'prompts', guideline);
97
+ const legacyPath = path.join('.claude', guideline);
98
+ if (fs.existsSync(promptsPath)) {
99
+ success(`${guideline}: present in .claude/prompts/`);
100
+ } else if (fs.existsSync(legacyPath)) {
101
+ warning(`${guideline}: present in .claude/ (should be in .claude/prompts/)`);
102
+ } else if (fs.existsSync(guideline)) {
103
+ warning(`${guideline}: present in root (should be in .claude/prompts/)`);
104
+ } else {
105
+ warning(`${guideline}: missing`);
106
+ }
107
+ });
108
+
109
+ // Verify entries in .gitignore
110
+ console.log('\n.gitignore:');
111
+ const gitignorePath = '.gitignore';
112
+ if (fs.existsSync(gitignorePath)) {
113
+ const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
114
+ const claudeIgnore = '.claude/';
115
+
116
+ const regex = new RegExp(`^${claudeIgnore.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm');
117
+ if (regex.test(gitignoreContent)) {
118
+ success(`${claudeIgnore}: included (protects all Claude files)`);
119
+ } else {
120
+ warning(`${claudeIgnore}: missing`);
121
+ info('\nRun "claude-hooks install" to update .gitignore');
122
+ }
123
+ } else {
124
+ warning('.gitignore doesn´t exist');
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Uninstall command
130
+ */
131
+ export function runUninstall() {
132
+ if (!checkGitRepo()) {
133
+ error('You are not in a Git repository.');
134
+ }
135
+
136
+ info('Uninstalling Claude Git Hooks...');
137
+
138
+ const hooksPath = '.git/hooks';
139
+ const hooks = ['pre-commit', 'prepare-commit-msg'];
140
+
141
+ hooks.forEach(hook => {
142
+ const hookPath = path.join(hooksPath, hook);
143
+ if (fs.existsSync(hookPath)) {
144
+ fs.unlinkSync(hookPath);
145
+ success(`${hook} removed`);
146
+ }
147
+ });
148
+
149
+ success('Claude Git Hooks uninstalled');
150
+ }