claude-git-hooks 2.9.1 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,103 @@
1
+ /**
2
+ * File: migrate-config.js
3
+ * Purpose: Migrate legacy config.json to v2.8.0 format
4
+ *
5
+ * DEPRECATED: Will be removed in v3.0.0 (most users will have migrated by then)
6
+ */
7
+
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import {
11
+ error,
12
+ success,
13
+ info,
14
+ warning
15
+ } from './helpers.js';
16
+ import { extractLegacySettings } from './install.js';
17
+
18
+ /**
19
+ * Migrates legacy config.json to v2.8.0 format (Manual command)
20
+ * Why: Simplifies configuration, reduces redundancy
21
+ */
22
+ export async function runMigrateConfig() {
23
+ const claudeDir = '.claude';
24
+ const configPath = path.join(claudeDir, 'config.json');
25
+
26
+ if (!fs.existsSync(configPath)) {
27
+ info('ℹ️ No config file found. Nothing to migrate.');
28
+ console.log('\n💡 To create a new config:');
29
+ console.log(' 1. Run: claude-hooks install --force');
30
+ console.log(' 2. Or copy from: .claude/config_example/config.example.json');
31
+ return;
32
+ }
33
+
34
+ try {
35
+ const rawConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
36
+
37
+ // Check if already in v2.8.0 format
38
+ if (rawConfig.version === '2.8.0') {
39
+ success('✅ Config is already in v2.8.0 format.');
40
+ return;
41
+ }
42
+
43
+ info('📦 Starting config migration to v2.8.0...');
44
+
45
+ // Create backup in config_old/
46
+ const configOldDir = path.join(claudeDir, 'config_old');
47
+ if (!fs.existsSync(configOldDir)) {
48
+ fs.mkdirSync(configOldDir, { recursive: true });
49
+ }
50
+ const backupPath = path.join(configOldDir, `config.json.${Date.now()}`);
51
+ fs.copyFileSync(configPath, backupPath);
52
+ success(`Backup created: ${backupPath}`);
53
+
54
+ // Extract only allowed parameters
55
+ const allowedOverrides = extractLegacySettings(rawConfig);
56
+
57
+ // Check for advanced params
58
+ const hasAdvancedParams = allowedOverrides.analysis?.ignoreExtensions ||
59
+ allowedOverrides.commitMessage?.taskIdPattern ||
60
+ allowedOverrides.subagents?.model;
61
+
62
+ // Build new config
63
+ const newConfig = {
64
+ version: '2.8.0',
65
+ preset: rawConfig.preset || 'default'
66
+ };
67
+
68
+ // Only add overrides if there are any
69
+ if (Object.keys(allowedOverrides).length > 0) {
70
+ newConfig.overrides = allowedOverrides;
71
+ }
72
+
73
+ // Show diff
74
+ console.log('\n📝 Migration preview:');
75
+ console.log(` Old format: ${Object.keys(rawConfig).length} top-level keys`);
76
+ console.log(` New format: ${Object.keys(newConfig).length} top-level keys`);
77
+ if (Object.keys(allowedOverrides).length > 0) {
78
+ console.log(` Preserved: ${Object.keys(allowedOverrides).length} override sections`);
79
+ }
80
+
81
+ // Write new config
82
+ fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 4));
83
+ success('✅ Config migrated to v2.8.0 successfully!');
84
+
85
+ if (hasAdvancedParams) {
86
+ warning('⚠️ Advanced parameters detected and preserved');
87
+ info('📖 See .claude/config.advanced.example.json for documentation');
88
+ }
89
+
90
+ console.log(`\n✨ New config:`);
91
+ console.log(JSON.stringify(newConfig, null, 2));
92
+ console.log(`\n💾 Old config backed up to: ${backupPath}`);
93
+ console.log('\n💡 Many parameters are now hardcoded with sensible defaults');
94
+ console.log(' See CHANGELOG.md for full list of changes');
95
+
96
+ } catch (err) {
97
+ error(`Failed to migrate config: ${err.message}`);
98
+ console.log('\n💡 Manual migration:');
99
+ console.log(' 1. Backup your current config');
100
+ console.log(' 2. See .claude/config.example.json for new format');
101
+ console.log(' 3. Copy minimal example and customize');
102
+ }
103
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * File: presets.js
3
+ * Purpose: Preset management commands (list, set, current)
4
+ */
5
+
6
+ import { listPresets } from '../utils/preset-loader.js';
7
+ import { getConfig } from '../config.js';
8
+ import {
9
+ colors,
10
+ error,
11
+ success,
12
+ info,
13
+ warning,
14
+ updateConfig
15
+ } from './helpers.js';
16
+
17
+ /**
18
+ * Shows all available presets
19
+ */
20
+ export async function runShowPresets() {
21
+ try {
22
+ const presets = await listPresets();
23
+
24
+ if (presets.length === 0) {
25
+ warning('No presets found');
26
+ return;
27
+ }
28
+
29
+ console.log('');
30
+ info('Available presets:');
31
+ console.log('');
32
+
33
+ presets.forEach(preset => {
34
+ console.log(` ${colors.green}${preset.name}${colors.reset}`);
35
+ console.log(` ${preset.displayName}`);
36
+ console.log(` ${colors.blue}${preset.description}${colors.reset}`);
37
+ console.log('');
38
+ });
39
+
40
+ info('To set a preset: claude-hooks --set-preset <name>');
41
+ info('To see current preset: claude-hooks preset current');
42
+ console.log('');
43
+ } catch (err) {
44
+ error(`Failed to list presets: ${err.message}`);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Sets the active preset
50
+ * Why: Configures tech-stack specific analysis settings
51
+ * @param {string} presetName - Name of preset to activate
52
+ */
53
+ export async function runSetPreset(presetName) {
54
+ if (!presetName) {
55
+ error('Please specify a preset name: claude-hooks --set-preset <name>');
56
+ return;
57
+ }
58
+
59
+ await updateConfig('preset', presetName, {
60
+ validator: async (name) => {
61
+ const presets = await listPresets();
62
+ const preset = presets.find(p => p.name === name);
63
+ if (!preset) {
64
+ error(`Preset "${name}" not found`);
65
+ info('Available presets:');
66
+ presets.forEach(p => console.log(` - ${p.name}`));
67
+ throw new Error(`Invalid preset: ${name}`);
68
+ }
69
+ return preset;
70
+ },
71
+ successMessage: async (name) => {
72
+ const presets = await listPresets();
73
+ const preset = presets.find(p => p.name === name);
74
+ return `Preset '${preset.displayName}' activated`;
75
+ }
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Shows the current active preset
81
+ */
82
+ export async function runCurrentPreset() {
83
+ try {
84
+ const config = await getConfig();
85
+ const presetName = config.preset || 'default';
86
+
87
+ const presets = await listPresets();
88
+ const preset = presets.find(p => p.name === presetName);
89
+
90
+ if (preset) {
91
+ console.log('');
92
+ success(`Current preset: ${preset.displayName} (${preset.name})`);
93
+ console.log(` ${colors.blue}${preset.description}${colors.reset}`);
94
+ console.log('');
95
+ } else {
96
+ warning(`Current preset "${presetName}" not found`);
97
+ }
98
+ } catch (err) {
99
+ error(`Failed to get current preset: ${err.message}`);
100
+ }
101
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * File: setup-github.js
3
+ * Purpose: Interactive GitHub authentication setup command
4
+ *
5
+ * Handles:
6
+ * - Checking existing token
7
+ * - Prompting user for new token
8
+ * - Validating token format and permissions
9
+ * - Saving token to .claude/settings.local.json
10
+ */
11
+
12
+ import { validateToken, saveGitHubToken, validateTokenFormat } from '../utils/github-api.js';
13
+ import { promptConfirmation, promptEditField, showSuccess, showError, showInfo, showWarning } from '../utils/interactive-ui.js';
14
+ import logger from '../utils/logger.js';
15
+
16
+ /**
17
+ * Run GitHub authentication setup
18
+ * Why: Interactive flow to configure GitHub token for PR creation
19
+ *
20
+ * @returns {Promise<void>}
21
+ */
22
+ export async function runSetupGitHub() {
23
+ showInfo('GitHub Authentication Setup');
24
+
25
+ // Check existing token
26
+ try {
27
+ const validation = await validateToken();
28
+ if (validation.valid) {
29
+ showSuccess(`Already authenticated as: ${validation.user}`);
30
+ logger.info(` Scopes: ${validation.scopes.join(', ')}`);
31
+
32
+ if (!validation.hasRepoScope) {
33
+ showWarning('Token lacks "repo" scope - PR creation may fail');
34
+ }
35
+
36
+ const reconfigure = await promptConfirmation('Configure a different token?', false);
37
+ if (!reconfigure) {
38
+ return;
39
+ }
40
+ }
41
+ } catch (e) {
42
+ logger.debug('setup-github', 'No existing token found or validation failed', { error: e.message });
43
+ }
44
+
45
+ // Show instructions
46
+ logger.info('Create a GitHub Personal Access Token:');
47
+ logger.info(' 1. Go to: https://github.com/settings/tokens/new');
48
+ logger.info(' 2. Select scopes: repo, read:org');
49
+ logger.info(' 3. Generate and copy the token');
50
+
51
+ // Prompt for token
52
+ const token = await promptEditField('Paste your token (ghp_...)', '');
53
+
54
+ if (!token || token.trim() === '') {
55
+ showWarning('No token provided. Setup cancelled.');
56
+ return;
57
+ }
58
+
59
+ const trimmedToken = token.trim();
60
+
61
+ // Validate format
62
+ const formatCheck = validateTokenFormat(trimmedToken);
63
+ if (formatCheck.warning) {
64
+ showWarning(formatCheck.warning);
65
+ const proceed = await promptConfirmation('Continue anyway?', false);
66
+ if (!proceed) {
67
+ return;
68
+ }
69
+ }
70
+
71
+ // Save token
72
+ const saveResult = saveGitHubToken(trimmedToken);
73
+ if (!saveResult.success) {
74
+ showError('Failed to save token: ' + saveResult.error);
75
+ return;
76
+ }
77
+ showSuccess(`Token saved to ${saveResult.path}`);
78
+
79
+ // Validate with GitHub API
80
+ showInfo('Validating token...');
81
+
82
+ const validation = await validateToken();
83
+ if (validation.valid) {
84
+ showSuccess(`Authenticated as: ${validation.user}`);
85
+ if (!validation.hasRepoScope) {
86
+ showWarning('Token lacks "repo" scope - regenerate with "repo" enabled');
87
+ }
88
+ } else {
89
+ showWarning('Token validation failed - may not work correctly');
90
+ }
91
+
92
+ showSuccess('Setup complete! You can now use: claude-hooks create-pr');
93
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * File: telemetry-cmd.js
3
+ * Purpose: Telemetry management commands (show, clear)
4
+ */
5
+
6
+ import { getConfig } from '../config.js';
7
+ import { displayStatistics as showTelemetryStats, clearTelemetry as clearTelemetryData } from '../utils/telemetry.js';
8
+ import { promptConfirmation } from '../utils/interactive-ui.js';
9
+ import logger from '../utils/logger.js';
10
+ import {
11
+ success,
12
+ info
13
+ } from './helpers.js';
14
+
15
+ /**
16
+ * Show telemetry statistics
17
+ * Why: Help users understand JSON parsing patterns and batch performance
18
+ */
19
+ export async function runShowTelemetry() {
20
+ await showTelemetryStats();
21
+ }
22
+
23
+ /**
24
+ * Clear telemetry data
25
+ * Why: Allow users to reset telemetry
26
+ */
27
+ export async function runClearTelemetry() {
28
+ const config = await getConfig();
29
+
30
+ if (!config.system?.telemetry && config.system?.telemetry !== undefined) {
31
+ logger.warning('⚠️ Telemetry is currently disabled.');
32
+ logger.info('To re-enable (default), remove or set to true in .claude/config.json:');
33
+ logger.info('{');
34
+ logger.info(' "system": {');
35
+ logger.info(' "telemetry": true');
36
+ logger.info(' }');
37
+ logger.info('}');
38
+ return;
39
+ }
40
+
41
+ const confirmed = await promptConfirmation('Are you sure you want to clear all telemetry data?');
42
+ if (confirmed) {
43
+ await clearTelemetryData();
44
+ success('Telemetry data cleared successfully');
45
+ } else {
46
+ info('Telemetry data was not cleared');
47
+ }
48
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * File: update.js
3
+ * Purpose: Update command - update to the latest version
4
+ */
5
+
6
+ import { execSync } from 'child_process';
7
+ import {
8
+ error,
9
+ success,
10
+ info,
11
+ warning,
12
+ getPackageJson,
13
+ getLatestVersion,
14
+ compareVersions
15
+ } from './helpers.js';
16
+ import { runInstall } from './install.js';
17
+
18
+ /**
19
+ * Update command - update to the latest version
20
+ */
21
+ export async function runUpdate() {
22
+ info('Checking latest available version...');
23
+
24
+ try {
25
+ const currentVersion = getPackageJson().version;
26
+ const latestVersion = await getLatestVersion('claude-git-hooks');
27
+
28
+ const comparison = compareVersions(currentVersion, latestVersion);
29
+
30
+ if (comparison === 0) {
31
+ success(`You already have the latest version installed (${currentVersion})`);
32
+ return;
33
+ } else if (comparison > 0) {
34
+ info(`You are using a development version (${currentVersion})`);
35
+ info(`Latest published version: ${latestVersion}`);
36
+ success(`You already have the latest version installed (${currentVersion})`);
37
+ return;
38
+ }
39
+
40
+ info(`Current version: ${currentVersion}`);
41
+ info(`Available version: ${latestVersion}`);
42
+
43
+ // Update the package
44
+ info('Updating claude-git-hooks...');
45
+ try {
46
+ execSync('npm install -g claude-git-hooks@latest', { stdio: 'inherit' });
47
+ success(`Successfully updated to version ${latestVersion}`);
48
+
49
+ // Reinstall hooks with the new version
50
+ info('Reinstalling hooks with the new version...');
51
+ await runInstall(['--force']);
52
+
53
+ } catch (updateError) {
54
+ error('Error updating. Try running: npm install -g claude-git-hooks@latest');
55
+ }
56
+ } catch (e) {
57
+ warning('Could not check the latest available version');
58
+ warning('Trying to update anyway...');
59
+ try {
60
+ execSync('npm install -g claude-git-hooks@latest', { stdio: 'inherit' });
61
+ success('Update completed');
62
+ await runInstall(['--force']);
63
+ } catch (updateError) {
64
+ error('Error updating: ' + updateError.message);
65
+ }
66
+ }
67
+ }
@@ -2,7 +2,7 @@
2
2
  * File: github-api.js
3
3
  * Purpose: Direct GitHub API integration via Octokit
4
4
  *
5
- * Why Octokit instead of MCP:
5
+ * Why Octokit:
6
6
  * - PR creation is deterministic (no AI judgment needed)
7
7
  * - Reliable error handling
8
8
  * - No external process dependencies
@@ -11,8 +11,7 @@
11
11
  * Token priority:
12
12
  * 1. GITHUB_TOKEN env var (CI/CD friendly)
13
13
  * 2. GITHUB_PERSONAL_ACCESS_TOKEN env var
14
- * 3. .claude/settings.local.json → githubToken
15
- * 4. Claude Desktop config (cross-platform)
14
+ * 3. .claude/settings.local.json → githubToken (local dev, gitignored)
16
15
  */
17
16
 
18
17
  import { Octokit } from '@octokit/rest';
@@ -21,7 +20,6 @@ import fs from 'fs';
21
20
  import path from 'path';
22
21
  import { fileURLToPath } from 'url';
23
22
  import logger from './logger.js';
24
- import { findGitHubTokenInDesktopConfig } from './mcp-setup.js';
25
23
 
26
24
  // Get package info for user agent
27
25
  const __filename = fileURLToPath(import.meta.url);
@@ -89,6 +87,89 @@ const loadLocalSettings = () => {
89
87
  return {};
90
88
  };
91
89
 
90
+ /**
91
+ * Save GitHub token to .claude/settings.local.json
92
+ * Why: Persist token in gitignored location for local development
93
+ *
94
+ * @param {string} token - GitHub Personal Access Token
95
+ * @returns {Object} { success: boolean, path: string, error?: string }
96
+ */
97
+ export const saveGitHubToken = (token) => {
98
+ try {
99
+ const repoRoot = getRepoRoot();
100
+ const claudeDir = path.join(repoRoot, '.claude');
101
+ const settingsPath = path.join(claudeDir, 'settings.local.json');
102
+
103
+ logger.debug('github-api - saveGitHubToken', 'Saving token', { settingsPath });
104
+
105
+ // Ensure .claude directory exists
106
+ if (!fs.existsSync(claudeDir)) {
107
+ fs.mkdirSync(claudeDir, { recursive: true });
108
+ logger.debug('github-api - saveGitHubToken', 'Created .claude directory');
109
+ }
110
+
111
+ // Load existing settings or create new
112
+ let settings = {};
113
+ if (fs.existsSync(settingsPath)) {
114
+ try {
115
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
116
+ } catch (e) {
117
+ logger.debug('github-api - saveGitHubToken', 'Invalid existing settings, starting fresh');
118
+ settings = {};
119
+ }
120
+ }
121
+
122
+ // Update token
123
+ settings.githubToken = token;
124
+
125
+ // Add comment if new file
126
+ if (!settings._comment) {
127
+ settings._comment = 'Local settings - DO NOT COMMIT. This file is gitignored.';
128
+ }
129
+
130
+ // Save settings
131
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
132
+
133
+ logger.debug('github-api - saveGitHubToken', 'Token saved successfully');
134
+
135
+ return { success: true, path: settingsPath };
136
+
137
+ } catch (error) {
138
+ logger.error('github-api - saveGitHubToken', 'Failed to save token', error);
139
+ return { success: false, path: null, error: error.message };
140
+ }
141
+ };
142
+
143
+ /**
144
+ * Validate GitHub token format
145
+ * Why: Catch obvious format errors before API call
146
+ *
147
+ * @param {string} token - Token to validate
148
+ * @returns {Object} { valid: boolean, warning?: string }
149
+ */
150
+ export const validateTokenFormat = (token) => {
151
+ if (!token || token.trim() === '') {
152
+ return { valid: false, warning: 'Token is empty' };
153
+ }
154
+
155
+ const trimmed = token.trim();
156
+
157
+ // Valid prefixes for GitHub tokens
158
+ if (trimmed.startsWith('ghp_') || trimmed.startsWith('github_pat_')) {
159
+ return { valid: true };
160
+ }
161
+
162
+ // Older token format (40 hex chars)
163
+ if (/^[a-f0-9]{40}$/i.test(trimmed)) {
164
+ return { valid: true };
165
+ }
166
+
167
+ return {
168
+ valid: true, // Still allow, just warn
169
+ warning: 'Token format looks unusual (expected ghp_... or github_pat_...)'
170
+ };
171
+ };
172
+
92
173
  /**
93
174
  * Get GitHub authentication token
94
175
  * Why: Centralized token resolution with multiple fallback sources
@@ -97,7 +178,6 @@ const loadLocalSettings = () => {
97
178
  * 1. GITHUB_TOKEN env var (standard for CI/CD)
98
179
  * 2. GITHUB_PERSONAL_ACCESS_TOKEN env var (legacy support)
99
180
  * 3. .claude/settings.local.json → githubToken (local dev, gitignored)
100
- * 4. Claude Desktop config (cross-platform GUI users)
101
181
  *
102
182
  * @returns {string} GitHub token
103
183
  * @throws {GitHubAPIError} If no token found
@@ -122,15 +202,6 @@ export const getGitHubToken = () => {
122
202
  return localSettings.githubToken;
123
203
  }
124
204
 
125
- // Priority 4: Claude Desktop config
126
- const desktopToken = findGitHubTokenInDesktopConfig();
127
- if (desktopToken?.token) {
128
- logger.debug('github-api - getGitHubToken', 'Using token from Claude Desktop config', {
129
- configPath: desktopToken.configPath
130
- });
131
- return desktopToken.token;
132
- }
133
-
134
205
  // No token found
135
206
  throw new GitHubAPIError(
136
207
  'GitHub token not found. Please configure authentication.',
@@ -139,10 +210,9 @@ export const getGitHubToken = () => {
139
210
  searchedLocations: [
140
211
  'GITHUB_TOKEN env var',
141
212
  'GITHUB_PERSONAL_ACCESS_TOKEN env var',
142
- '.claude/settings.local.json → githubToken',
143
- 'Claude Desktop config'
213
+ '.claude/settings.local.json → githubToken'
144
214
  ],
145
- suggestion: 'Run: claude-hooks setup-github or create .claude/settings.local.json with {"githubToken": "ghp_..."}'
215
+ suggestion: 'Run: claude-hooks setup-github'
146
216
  }
147
217
  }
148
218
  );