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.
- package/CHANGELOG.md +157 -0
- package/README.md +209 -749
- package/bin/claude-hooks +97 -2235
- 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/config.js +1 -0
- package/lib/hooks/pre-commit.js +21 -2
- package/lib/hooks/prepare-commit-msg.js +13 -1
- package/lib/utils/claude-client.js +103 -20
- package/lib/utils/github-api.js +87 -17
- package/lib/utils/github-client.js +9 -550
- package/lib/utils/prompt-builder.js +10 -9
- package/lib/utils/resolution-prompt.js +2 -2
- package/lib/utils/telemetry.js +507 -0
- package/package.json +1 -1
- package/lib/utils/mcp-setup.js +0 -342
|
@@ -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
|
+
}
|