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
|
@@ -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
|
+
}
|
package/lib/utils/github-api.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* File: github-api.js
|
|
3
3
|
* Purpose: Direct GitHub API integration via Octokit
|
|
4
4
|
*
|
|
5
|
-
* Why Octokit
|
|
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
|
|
215
|
+
suggestion: 'Run: claude-hooks setup-github'
|
|
146
216
|
}
|
|
147
217
|
}
|
|
148
218
|
);
|