claude-git-hooks 2.18.0 → 2.19.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 +38 -0
- package/CLAUDE.md +12 -8
- package/README.md +2 -1
- package/bin/claude-hooks +75 -89
- package/lib/cli-metadata.js +301 -0
- package/lib/commands/analyze-diff.js +12 -10
- package/lib/commands/analyze.js +9 -5
- package/lib/commands/bump-version.js +66 -43
- package/lib/commands/create-pr.js +71 -34
- package/lib/commands/debug.js +4 -7
- package/lib/commands/generate-changelog.js +11 -4
- package/lib/commands/help.js +47 -27
- package/lib/commands/helpers.js +66 -43
- package/lib/commands/hooks.js +15 -13
- package/lib/commands/install.js +546 -39
- package/lib/commands/migrate-config.js +8 -11
- package/lib/commands/presets.js +6 -13
- package/lib/commands/setup-github.js +12 -3
- package/lib/commands/telemetry-cmd.js +8 -6
- package/lib/commands/update.js +1 -2
- package/lib/config.js +36 -31
- package/lib/hooks/pre-commit.js +34 -54
- package/lib/hooks/prepare-commit-msg.js +39 -58
- package/lib/utils/analysis-engine.js +28 -21
- package/lib/utils/changelog-generator.js +162 -34
- package/lib/utils/claude-client.js +438 -377
- package/lib/utils/claude-diagnostics.js +20 -10
- package/lib/utils/file-operations.js +51 -79
- package/lib/utils/file-utils.js +46 -9
- package/lib/utils/git-operations.js +140 -123
- package/lib/utils/git-tag-manager.js +24 -23
- package/lib/utils/github-api.js +85 -61
- package/lib/utils/github-client.js +12 -14
- package/lib/utils/installation-diagnostics.js +4 -4
- package/lib/utils/interactive-ui.js +29 -17
- package/lib/utils/logger.js +4 -1
- package/lib/utils/pr-metadata-engine.js +67 -33
- package/lib/utils/preset-loader.js +20 -62
- package/lib/utils/prompt-builder.js +50 -55
- package/lib/utils/resolution-prompt.js +33 -44
- package/lib/utils/sanitize.js +20 -19
- package/lib/utils/task-id.js +27 -40
- package/lib/utils/telemetry.js +29 -17
- package/lib/utils/version-manager.js +173 -126
- package/lib/utils/which-command.js +23 -12
- package/package.json +69 -69
package/lib/commands/install.js
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
Entertainment
|
|
32
32
|
} from './helpers.js';
|
|
33
33
|
import { runSetupGitHub } from './setup-github.js';
|
|
34
|
+
import { generateCompletionData } from '../cli-metadata.js';
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* Function to check version (used by hooks)
|
|
@@ -65,7 +66,7 @@ async function checkVersionAndPromptUpdate() {
|
|
|
65
66
|
success('Update completed. Please run your command again.');
|
|
66
67
|
process.exit(0); // Exit so user restarts the process
|
|
67
68
|
} catch (e) {
|
|
68
|
-
error(`Error updating: ${
|
|
69
|
+
error(`Error updating: ${e.message}`);
|
|
69
70
|
resolve(false);
|
|
70
71
|
}
|
|
71
72
|
} else {
|
|
@@ -96,10 +97,14 @@ async function checkAndInstallClaude() {
|
|
|
96
97
|
if (isWindows()) {
|
|
97
98
|
console.log('\n⚠️ On Windows, Claude CLI must be installed in WSL:');
|
|
98
99
|
console.log('1. Open WSL terminal (wsl or Ubuntu from Start Menu)');
|
|
99
|
-
console.log(
|
|
100
|
+
console.log(
|
|
101
|
+
'2. Follow installation at: https://docs.anthropic.com/claude/docs/claude-cli'
|
|
102
|
+
);
|
|
100
103
|
console.log('3. Verify with: wsl claude --version');
|
|
101
104
|
} else {
|
|
102
|
-
console.log(
|
|
105
|
+
console.log(
|
|
106
|
+
'\nClaude CLI installation: https://docs.anthropic.com/claude/docs/claude-cli'
|
|
107
|
+
);
|
|
103
108
|
}
|
|
104
109
|
|
|
105
110
|
console.log('\nAfter installation, run: claude-hooks install --force');
|
|
@@ -211,7 +216,7 @@ function updateGitignore() {
|
|
|
211
216
|
const gitignorePath = '.gitignore';
|
|
212
217
|
const claudeEntries = [
|
|
213
218
|
'# Claude Git Hooks (includes .claude/settings.local.json for tokens)',
|
|
214
|
-
'.claude/'
|
|
219
|
+
'.claude/'
|
|
215
220
|
];
|
|
216
221
|
|
|
217
222
|
let gitignoreContent = '';
|
|
@@ -225,7 +230,7 @@ function updateGitignore() {
|
|
|
225
230
|
|
|
226
231
|
// Check which entries are missing
|
|
227
232
|
const missingEntries = [];
|
|
228
|
-
claudeEntries.forEach(entry => {
|
|
233
|
+
claudeEntries.forEach((entry) => {
|
|
229
234
|
if (entry.startsWith('#')) {
|
|
230
235
|
// For comments, check if any Claude comment already exists
|
|
231
236
|
if (!gitignoreContent.includes('# Claude')) {
|
|
@@ -253,7 +258,7 @@ function updateGitignore() {
|
|
|
253
258
|
}
|
|
254
259
|
|
|
255
260
|
// Add the missing entries
|
|
256
|
-
gitignoreContent += `${missingEntries.join('\n')
|
|
261
|
+
gitignoreContent += `${missingEntries.join('\n')}\n`;
|
|
257
262
|
|
|
258
263
|
// Write the updated file
|
|
259
264
|
fs.writeFileSync(gitignorePath, gitignoreContent);
|
|
@@ -265,7 +270,7 @@ function updateGitignore() {
|
|
|
265
270
|
}
|
|
266
271
|
|
|
267
272
|
// Show what was added
|
|
268
|
-
missingEntries.forEach(entry => {
|
|
273
|
+
missingEntries.forEach((entry) => {
|
|
269
274
|
if (!entry.startsWith('#')) {
|
|
270
275
|
info(` + ${entry}`);
|
|
271
276
|
}
|
|
@@ -293,9 +298,8 @@ function configureGit() {
|
|
|
293
298
|
execSync('git config core.autocrlf input', { stdio: 'ignore' });
|
|
294
299
|
success('Line endings configured for Unix (core.autocrlf = input)');
|
|
295
300
|
}
|
|
296
|
-
|
|
297
301
|
} catch (e) {
|
|
298
|
-
warning(`Error configuring Git: ${
|
|
302
|
+
warning(`Error configuring Git: ${e.message}`);
|
|
299
303
|
}
|
|
300
304
|
}
|
|
301
305
|
|
|
@@ -381,7 +385,9 @@ async function autoMigrateConfig(newConfigPath, backupConfigPath) {
|
|
|
381
385
|
*/
|
|
382
386
|
export async function runInstall(args) {
|
|
383
387
|
if (!checkGitRepo()) {
|
|
384
|
-
error(
|
|
388
|
+
error(
|
|
389
|
+
'You are not in a Git repository. Please run this command from the root of a repository.'
|
|
390
|
+
);
|
|
385
391
|
}
|
|
386
392
|
|
|
387
393
|
const isForce = args.includes('--force');
|
|
@@ -422,7 +428,7 @@ export async function runInstall(args) {
|
|
|
422
428
|
// Hooks to install
|
|
423
429
|
const hooks = ['pre-commit', 'prepare-commit-msg'];
|
|
424
430
|
|
|
425
|
-
hooks.forEach(hook => {
|
|
431
|
+
hooks.forEach((hook) => {
|
|
426
432
|
const sourcePath = path.join(templatesPath, hook);
|
|
427
433
|
const destPath = path.join(hooksPath, hook);
|
|
428
434
|
|
|
@@ -457,12 +463,9 @@ export async function runInstall(args) {
|
|
|
457
463
|
}
|
|
458
464
|
|
|
459
465
|
// Remove old SONAR template files if they exist (migration from v2.6.x to v2.7.0+)
|
|
460
|
-
const oldSonarFiles = [
|
|
461
|
-
'CLAUDE_PRE_COMMIT_SONAR.md',
|
|
462
|
-
'CLAUDE_ANALYSIS_PROMPT_SONAR.md'
|
|
463
|
-
];
|
|
466
|
+
const oldSonarFiles = ['CLAUDE_PRE_COMMIT_SONAR.md', 'CLAUDE_ANALYSIS_PROMPT_SONAR.md'];
|
|
464
467
|
|
|
465
|
-
oldSonarFiles.forEach(oldFile => {
|
|
468
|
+
oldSonarFiles.forEach((oldFile) => {
|
|
466
469
|
const oldPath = path.join(claudeDir, oldFile);
|
|
467
470
|
if (fs.existsSync(oldPath)) {
|
|
468
471
|
fs.unlinkSync(oldPath);
|
|
@@ -478,16 +481,17 @@ export async function runInstall(args) {
|
|
|
478
481
|
}
|
|
479
482
|
|
|
480
483
|
// Copy template files (.md and .json) to appropriate locations
|
|
481
|
-
const templateFiles = fs.readdirSync(templatesPath)
|
|
482
|
-
.
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
484
|
+
const templateFiles = fs.readdirSync(templatesPath).filter((file) => {
|
|
485
|
+
const filePath = path.join(templatesPath, file);
|
|
486
|
+
// Exclude example.json files and only include .md and .json files
|
|
487
|
+
return (
|
|
488
|
+
fs.statSync(filePath).isFile() &&
|
|
489
|
+
(file.endsWith('.md') || file.endsWith('.json')) &&
|
|
490
|
+
!file.includes('example.json')
|
|
491
|
+
);
|
|
492
|
+
});
|
|
489
493
|
|
|
490
|
-
templateFiles.forEach(file => {
|
|
494
|
+
templateFiles.forEach((file) => {
|
|
491
495
|
const sourcePath = path.join(templatesPath, file);
|
|
492
496
|
let destPath;
|
|
493
497
|
let destLocation;
|
|
@@ -514,14 +518,13 @@ export async function runInstall(args) {
|
|
|
514
518
|
|
|
515
519
|
// Clean up old .md files from .claude/ root (v2.8.0 migration)
|
|
516
520
|
// .md files should now be in .claude/prompts/, not .claude/
|
|
517
|
-
const oldMdFiles = fs.readdirSync(claudeDir)
|
|
518
|
-
.
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
});
|
|
521
|
+
const oldMdFiles = fs.readdirSync(claudeDir).filter((file) => {
|
|
522
|
+
const filePath = path.join(claudeDir, file);
|
|
523
|
+
return fs.statSync(filePath).isFile() && file.endsWith('.md');
|
|
524
|
+
});
|
|
522
525
|
|
|
523
526
|
if (oldMdFiles.length > 0) {
|
|
524
|
-
oldMdFiles.forEach(file => {
|
|
527
|
+
oldMdFiles.forEach((file) => {
|
|
525
528
|
const oldPath = path.join(claudeDir, file);
|
|
526
529
|
fs.unlinkSync(oldPath);
|
|
527
530
|
info(`Removed old template from .claude/: ${file} (now in prompts/)`);
|
|
@@ -539,10 +542,11 @@ export async function runInstall(args) {
|
|
|
539
542
|
}
|
|
540
543
|
|
|
541
544
|
// Copy each preset directory
|
|
542
|
-
const presetDirs = fs
|
|
543
|
-
.
|
|
545
|
+
const presetDirs = fs
|
|
546
|
+
.readdirSync(presetsSourcePath)
|
|
547
|
+
.filter((item) => fs.statSync(path.join(presetsSourcePath, item)).isDirectory());
|
|
544
548
|
|
|
545
|
-
presetDirs.forEach(presetName => {
|
|
549
|
+
presetDirs.forEach((presetName) => {
|
|
546
550
|
const presetSource = path.join(presetsSourcePath, presetName);
|
|
547
551
|
const presetDest = path.join(presetsDestPath, presetName);
|
|
548
552
|
|
|
@@ -553,7 +557,7 @@ export async function runInstall(args) {
|
|
|
553
557
|
|
|
554
558
|
// Copy all files in preset directory
|
|
555
559
|
const presetFiles = fs.readdirSync(presetSource);
|
|
556
|
-
presetFiles.forEach(file => {
|
|
560
|
+
presetFiles.forEach((file) => {
|
|
557
561
|
const sourceFile = path.join(presetSource, file);
|
|
558
562
|
const destFile = path.join(presetDest, file);
|
|
559
563
|
|
|
@@ -585,7 +589,7 @@ export async function runInstall(args) {
|
|
|
585
589
|
|
|
586
590
|
// Copy example configs to config_example/ directly from templates/
|
|
587
591
|
const exampleConfigs = ['config.example.json', 'config.advanced.example.json'];
|
|
588
|
-
exampleConfigs.forEach(exampleFile => {
|
|
592
|
+
exampleConfigs.forEach((exampleFile) => {
|
|
589
593
|
const sourcePath = path.join(templatesPath, exampleFile);
|
|
590
594
|
const destPath = path.join(configExampleDir, exampleFile);
|
|
591
595
|
if (fs.existsSync(sourcePath)) {
|
|
@@ -634,7 +638,10 @@ export async function runInstall(args) {
|
|
|
634
638
|
// Auto-run migration if needed to preserve settings
|
|
635
639
|
if (needsMigration) {
|
|
636
640
|
info('🔄 Auto-migrating settings from backup...');
|
|
637
|
-
await autoMigrateConfig(
|
|
641
|
+
await autoMigrateConfig(
|
|
642
|
+
configPath,
|
|
643
|
+
path.join(configOldDir, fs.readdirSync(configOldDir).sort().pop())
|
|
644
|
+
);
|
|
638
645
|
}
|
|
639
646
|
}
|
|
640
647
|
|
|
@@ -642,8 +649,8 @@ export async function runInstall(args) {
|
|
|
642
649
|
const settingsLocalPath = path.join(claudeDir, 'settings.local.json');
|
|
643
650
|
if (!fs.existsSync(settingsLocalPath)) {
|
|
644
651
|
const settingsLocalContent = {
|
|
645
|
-
|
|
646
|
-
|
|
652
|
+
_comment: 'Local settings - DO NOT COMMIT. This file is gitignored.',
|
|
653
|
+
githubToken: ''
|
|
647
654
|
};
|
|
648
655
|
fs.writeFileSync(settingsLocalPath, JSON.stringify(settingsLocalContent, null, 2));
|
|
649
656
|
info('settings.local.json created (add your GitHub token here)');
|
|
@@ -655,6 +662,9 @@ export async function runInstall(args) {
|
|
|
655
662
|
// Update .gitignore
|
|
656
663
|
updateGitignore();
|
|
657
664
|
|
|
665
|
+
// Install shell completions
|
|
666
|
+
installCompletions();
|
|
667
|
+
|
|
658
668
|
success('Claude Git Hooks installed successfully! 🎉');
|
|
659
669
|
console.log('\nRun claude-hooks --help to see all available commands.');
|
|
660
670
|
|
|
@@ -662,3 +672,500 @@ export async function runInstall(args) {
|
|
|
662
672
|
console.log('');
|
|
663
673
|
await runSetupGitHub();
|
|
664
674
|
}
|
|
675
|
+
|
|
676
|
+
// ── Shell completion generation ──────────────────────────────────────────────
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* User-level paths for completion scripts
|
|
680
|
+
* @returns {{ bash: string, zsh: string, fish: string, powershell: string }}
|
|
681
|
+
*/
|
|
682
|
+
export function getCompletionPaths() {
|
|
683
|
+
const home = os.homedir();
|
|
684
|
+
return {
|
|
685
|
+
bash: path.join(home, '.local', 'share', 'bash-completion', 'completions', 'claude-hooks'),
|
|
686
|
+
zsh: path.join(home, '.zfunc', '_claude-hooks'),
|
|
687
|
+
fish: path.join(home, '.config', 'fish', 'completions', 'claude-hooks.fish'),
|
|
688
|
+
powershell: path.join(home, '.config', 'powershell', 'completions', 'claude-hooks.ps1')
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Get PowerShell profile path by querying PowerShell's $PROFILE variable.
|
|
694
|
+
* Why: Hardcoding ~/Documents/PowerShell/ breaks on OneDrive-redirected or locale-specific
|
|
695
|
+
* Documents folders (e.g., "OneDrive - Corp/Documentos/WindowsPowerShell/").
|
|
696
|
+
* Falls back to conventional paths if the query fails.
|
|
697
|
+
* @returns {string}
|
|
698
|
+
*/
|
|
699
|
+
export function getPowerShellProfilePath() {
|
|
700
|
+
const home = os.homedir();
|
|
701
|
+
const isWindows = os.platform() === 'win32' || process.env.OS === 'Windows_NT';
|
|
702
|
+
|
|
703
|
+
if (isWindows) {
|
|
704
|
+
// Ask PowerShell itself for the correct $PROFILE path
|
|
705
|
+
// This handles OneDrive redirection, localized folder names, and pwsh vs PS 5.1
|
|
706
|
+
for (const shell of ['pwsh', 'powershell.exe']) {
|
|
707
|
+
try {
|
|
708
|
+
const profilePath = execSync(`${shell} -NoProfile -Command "$PROFILE"`, {
|
|
709
|
+
encoding: 'utf8',
|
|
710
|
+
timeout: 5000,
|
|
711
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
712
|
+
}).trim();
|
|
713
|
+
if (profilePath && profilePath.endsWith('.ps1')) {
|
|
714
|
+
return profilePath;
|
|
715
|
+
}
|
|
716
|
+
} catch {
|
|
717
|
+
// Shell not available or timed out, try next
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
// Fallback: conventional paths if PowerShell query fails
|
|
721
|
+
const psCorePath = path.join(
|
|
722
|
+
home,
|
|
723
|
+
'Documents',
|
|
724
|
+
'PowerShell',
|
|
725
|
+
'Microsoft.PowerShell_profile.ps1'
|
|
726
|
+
);
|
|
727
|
+
if (fs.existsSync(path.dirname(psCorePath))) {
|
|
728
|
+
return psCorePath;
|
|
729
|
+
}
|
|
730
|
+
return path.join(
|
|
731
|
+
home,
|
|
732
|
+
'Documents',
|
|
733
|
+
'WindowsPowerShell',
|
|
734
|
+
'Microsoft.PowerShell_profile.ps1'
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
return path.join(home, '.config', 'powershell', 'Microsoft.PowerShell_profile.ps1');
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Generate Bash completion script from completion data
|
|
742
|
+
* @param {Object} data - Flat completion data from generateCompletionData()
|
|
743
|
+
* @returns {string}
|
|
744
|
+
*/
|
|
745
|
+
export function generateBashCompletion(data) {
|
|
746
|
+
const allCommands = data.commands.join(' ');
|
|
747
|
+
|
|
748
|
+
// Build per-command flag/arg cases
|
|
749
|
+
const cases = [];
|
|
750
|
+
for (const cmd of data.commands) {
|
|
751
|
+
const parts = [];
|
|
752
|
+
if (data.flags[cmd]) {
|
|
753
|
+
parts.push(...data.flags[cmd]);
|
|
754
|
+
}
|
|
755
|
+
if (data.subcommands[cmd]) {
|
|
756
|
+
parts.push(...data.subcommands[cmd]);
|
|
757
|
+
}
|
|
758
|
+
if (data.argValues[cmd]) {
|
|
759
|
+
parts.push(...data.argValues[cmd]);
|
|
760
|
+
}
|
|
761
|
+
if (parts.length > 0) {
|
|
762
|
+
cases.push(
|
|
763
|
+
` ${cmd})\n COMPREPLY=( $(compgen -W "${parts.join(' ')}" -- "$cur") )\n return 0\n ;;`
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
// Dynamic branch completion
|
|
767
|
+
if (data.argCompletions[cmd]) {
|
|
768
|
+
cases.push(
|
|
769
|
+
` ${cmd})\n local branches\n branches=$(${data.argCompletions[cmd]} 2>/dev/null)\n COMPREPLY=( $(compgen -W "$branches" -- "$cur") )\n return 0\n ;;`
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
return `# Bash completion for claude-hooks
|
|
775
|
+
# Generated by claude-hooks install — do not edit manually
|
|
776
|
+
_claude_hooks_completions() {
|
|
777
|
+
local cur prev commands
|
|
778
|
+
COMPREPLY=()
|
|
779
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
780
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
781
|
+
commands="${allCommands}"
|
|
782
|
+
|
|
783
|
+
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
784
|
+
COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
|
|
785
|
+
return 0
|
|
786
|
+
fi
|
|
787
|
+
|
|
788
|
+
case "$prev" in
|
|
789
|
+
${cases.join('\n')}
|
|
790
|
+
esac
|
|
791
|
+
|
|
792
|
+
return 0
|
|
793
|
+
}
|
|
794
|
+
complete -F _claude_hooks_completions claude-hooks
|
|
795
|
+
`;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Generate Zsh completion script from completion data
|
|
800
|
+
* @param {Object} data - Flat completion data from generateCompletionData()
|
|
801
|
+
* @returns {string}
|
|
802
|
+
*/
|
|
803
|
+
export function generateZshCompletion(data) {
|
|
804
|
+
// Build command descriptions for _describe
|
|
805
|
+
const cmdDescs = data.commands
|
|
806
|
+
.filter((c) => !c.startsWith('-'))
|
|
807
|
+
.map((c) => `'${c}:${(data.descriptions[c] || '').replace(/'/g, "\\'")}'`)
|
|
808
|
+
.join('\n ');
|
|
809
|
+
|
|
810
|
+
// Build per-command completions
|
|
811
|
+
const subcases = [];
|
|
812
|
+
for (const cmd of data.commands) {
|
|
813
|
+
if (cmd.startsWith('-')) continue;
|
|
814
|
+
const parts = [];
|
|
815
|
+
if (data.flags[cmd]) {
|
|
816
|
+
for (const flag of data.flags[cmd]) {
|
|
817
|
+
const desc = '';
|
|
818
|
+
parts.push(`'${flag}[${desc}]'`);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
if (data.subcommands[cmd]) {
|
|
822
|
+
parts.push(`'(${data.subcommands[cmd].join(' ')})'`);
|
|
823
|
+
}
|
|
824
|
+
if (data.argValues[cmd]) {
|
|
825
|
+
parts.push(`'(${data.argValues[cmd].join(' ')})'`);
|
|
826
|
+
}
|
|
827
|
+
if (data.argCompletions[cmd]) {
|
|
828
|
+
subcases.push(
|
|
829
|
+
` ${cmd})\n local branches\n branches=(\${(f)"$(${data.argCompletions[cmd]} 2>/dev/null)"})\n _describe 'branch' branches\n ;;`
|
|
830
|
+
);
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
if (parts.length > 0) {
|
|
834
|
+
subcases.push(
|
|
835
|
+
` ${cmd})\n _arguments ${parts.join(' ')}\n ;;`
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
return `#compdef claude-hooks
|
|
841
|
+
# Zsh completion for claude-hooks
|
|
842
|
+
# Generated by claude-hooks install — do not edit manually
|
|
843
|
+
|
|
844
|
+
_claude-hooks() {
|
|
845
|
+
local -a commands
|
|
846
|
+
commands=(
|
|
847
|
+
${cmdDescs}
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
_arguments '1:command:->cmds' '*:arg:->args'
|
|
851
|
+
|
|
852
|
+
case $state in
|
|
853
|
+
cmds)
|
|
854
|
+
_describe 'command' commands
|
|
855
|
+
;;
|
|
856
|
+
args)
|
|
857
|
+
case $words[2] in
|
|
858
|
+
${subcases.join('\n')}
|
|
859
|
+
esac
|
|
860
|
+
;;
|
|
861
|
+
esac
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
_claude-hooks "$@"
|
|
865
|
+
`;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Generate Fish completion script from completion data
|
|
870
|
+
* @param {Object} data - Flat completion data from generateCompletionData()
|
|
871
|
+
* @returns {string}
|
|
872
|
+
*/
|
|
873
|
+
export function generateFishCompletion(data) {
|
|
874
|
+
const lines = [
|
|
875
|
+
'# Fish completion for claude-hooks',
|
|
876
|
+
'# Generated by claude-hooks install — do not edit manually',
|
|
877
|
+
'',
|
|
878
|
+
'# Disable file completions by default',
|
|
879
|
+
'complete -c claude-hooks -f',
|
|
880
|
+
''
|
|
881
|
+
];
|
|
882
|
+
|
|
883
|
+
// Top-level commands
|
|
884
|
+
for (const cmd of data.commands) {
|
|
885
|
+
if (cmd.startsWith('-')) continue;
|
|
886
|
+
const desc = data.descriptions[cmd] || '';
|
|
887
|
+
lines.push(
|
|
888
|
+
`complete -c claude-hooks -n '__fish_use_subcommand' -a '${cmd}' -d '${desc.replace(/'/g, "\\'")}'`
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
lines.push('');
|
|
893
|
+
|
|
894
|
+
// Per-command flags and subcommands
|
|
895
|
+
for (const cmd of data.commands) {
|
|
896
|
+
if (cmd.startsWith('-')) continue;
|
|
897
|
+
|
|
898
|
+
if (data.flags[cmd]) {
|
|
899
|
+
for (const flag of data.flags[cmd]) {
|
|
900
|
+
const flagName = flag.replace(/^--/, '');
|
|
901
|
+
lines.push(
|
|
902
|
+
`complete -c claude-hooks -n '__fish_seen_subcommand_from ${cmd}' -l '${flagName}' -d '${(data.descriptions[cmd] || '').replace(/'/g, "\\'")}'`
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
if (data.subcommands[cmd]) {
|
|
908
|
+
for (const sub of data.subcommands[cmd]) {
|
|
909
|
+
lines.push(
|
|
910
|
+
`complete -c claude-hooks -n '__fish_seen_subcommand_from ${cmd}' -a '${sub}'`
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
if (data.argValues[cmd]) {
|
|
916
|
+
for (const val of data.argValues[cmd]) {
|
|
917
|
+
lines.push(
|
|
918
|
+
`complete -c claude-hooks -n '__fish_seen_subcommand_from ${cmd}' -a '${val}'`
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
if (data.argCompletions[cmd]) {
|
|
924
|
+
lines.push(
|
|
925
|
+
`complete -c claude-hooks -n '__fish_seen_subcommand_from ${cmd}' -a '(${data.argCompletions[cmd]} 2>/dev/null)'`
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
lines.push('');
|
|
931
|
+
return lines.join('\n');
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* Generate PowerShell completion script from completion data
|
|
936
|
+
* @param {Object} data - Flat completion data from generateCompletionData()
|
|
937
|
+
* @returns {string}
|
|
938
|
+
*/
|
|
939
|
+
export function generatePowerShellCompletion(data) {
|
|
940
|
+
const allCommands = data.commands.map((c) => `'${c}'`).join(', ');
|
|
941
|
+
|
|
942
|
+
// Build per-command switch cases
|
|
943
|
+
const cases = [];
|
|
944
|
+
for (const cmd of data.commands) {
|
|
945
|
+
const completions = [];
|
|
946
|
+
if (data.flags[cmd]) {
|
|
947
|
+
for (const flag of data.flags[cmd]) {
|
|
948
|
+
completions.push(
|
|
949
|
+
`[System.Management.Automation.CompletionResult]::new('${flag}', '${flag}', [System.Management.Automation.CompletionResultType]::ParameterName, '${(data.descriptions[cmd] || '').replace(/'/g, "''")}')`
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
if (data.subcommands[cmd]) {
|
|
954
|
+
for (const sub of data.subcommands[cmd]) {
|
|
955
|
+
completions.push(
|
|
956
|
+
`[System.Management.Automation.CompletionResult]::new('${sub}', '${sub}', [System.Management.Automation.CompletionResultType]::ParameterValue, '${sub}')`
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
if (data.argValues[cmd]) {
|
|
961
|
+
for (const val of data.argValues[cmd]) {
|
|
962
|
+
completions.push(
|
|
963
|
+
`[System.Management.Automation.CompletionResult]::new('${val}', '${val}', [System.Management.Automation.CompletionResultType]::ParameterValue, '${val}')`
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
if (completions.length > 0) {
|
|
968
|
+
cases.push(
|
|
969
|
+
` '${cmd}' {\n ${completions.join('\n ')}\n }`
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
return `# PowerShell completion for claude-hooks
|
|
975
|
+
# Generated by claude-hooks install — do not edit manually
|
|
976
|
+
|
|
977
|
+
# PS 5.1 workaround: -Native completers don't fire for ExternalScript commands (.ps1 npm shims).
|
|
978
|
+
# Wrapping in a function makes PowerShell resolve it as Function type, which -Native completers support.
|
|
979
|
+
if ((Get-Command claude-hooks -ErrorAction SilentlyContinue).CommandType -eq 'ExternalScript') {
|
|
980
|
+
$script:_claudeHooksOriginalPath = (Get-Command claude-hooks -CommandType ExternalScript).Source
|
|
981
|
+
function global:claude-hooks { & $script:_claudeHooksOriginalPath @args }
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
Register-ArgumentCompleter -CommandName claude-hooks -Native -ScriptBlock {
|
|
985
|
+
param($wordToComplete, $commandAst, $cursorPosition)
|
|
986
|
+
$commands = @(${allCommands})
|
|
987
|
+
$tokens = $commandAst.ToString().Split()
|
|
988
|
+
# .Split() drops trailing empty entries — detect cursor past last token
|
|
989
|
+
$position = $tokens.Count
|
|
990
|
+
if ($commandAst.ToString() -match '\\s$') { $position++ }
|
|
991
|
+
|
|
992
|
+
if ($position -le 2) {
|
|
993
|
+
$commands | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
994
|
+
[System.Management.Automation.CompletionResult]::new($_, $_, [System.Management.Automation.CompletionResultType]::ParameterValue, $_)
|
|
995
|
+
}
|
|
996
|
+
return
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
$cmd = $tokens[1]
|
|
1000
|
+
switch ($cmd) {
|
|
1001
|
+
${cases.join('\n')}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
`;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Ensure a marker+line pair exists in an rc file, replacing stale content if needed.
|
|
1009
|
+
* Idempotent: skips write if marker and line already match exactly.
|
|
1010
|
+
* Self-healing: replaces the line after the marker if content has changed (e.g., backslash → $HOME paths).
|
|
1011
|
+
* @param {string} filePath - Absolute path to rc file
|
|
1012
|
+
* @param {string} marker - Unique comment marker to identify our block
|
|
1013
|
+
* @param {string} line - The line to place after the marker
|
|
1014
|
+
*/
|
|
1015
|
+
function appendLineIfMissing(filePath, marker, line) {
|
|
1016
|
+
let content = '';
|
|
1017
|
+
if (fs.existsSync(filePath)) {
|
|
1018
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
1019
|
+
}
|
|
1020
|
+
if (content.includes(marker)) {
|
|
1021
|
+
// Marker exists — check if the line after it matches the desired content
|
|
1022
|
+
const markerLineRegex = new RegExp(
|
|
1023
|
+
`${marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\n[^\\n]*`
|
|
1024
|
+
);
|
|
1025
|
+
const match = content.match(markerLineRegex);
|
|
1026
|
+
if (match && match[0] === `${marker}\n${line}`) {
|
|
1027
|
+
return; // Already correct
|
|
1028
|
+
}
|
|
1029
|
+
// Replace stale line after marker with the correct one
|
|
1030
|
+
content = content.replace(markerLineRegex, `${marker}\n${line}`);
|
|
1031
|
+
fs.writeFileSync(filePath, content);
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
const newline = content.length > 0 && !content.endsWith('\n') ? '\n' : '';
|
|
1035
|
+
fs.appendFileSync(filePath, `${newline}\n${marker}\n${line}\n`);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Install shell completion scripts for all supported shells
|
|
1040
|
+
* Reads command metadata and generates completion scripts to user-level paths.
|
|
1041
|
+
* Uses $HOME-relative paths in source lines to avoid backslash issues on Windows/MINGW64.
|
|
1042
|
+
* Wrapped in try/catch — warns on failure, never blocks install.
|
|
1043
|
+
*/
|
|
1044
|
+
export function installCompletions() {
|
|
1045
|
+
try {
|
|
1046
|
+
const data = generateCompletionData();
|
|
1047
|
+
const paths = getCompletionPaths();
|
|
1048
|
+
let installed = 0;
|
|
1049
|
+
|
|
1050
|
+
// Bash
|
|
1051
|
+
try {
|
|
1052
|
+
fs.mkdirSync(path.dirname(paths.bash), { recursive: true });
|
|
1053
|
+
fs.writeFileSync(paths.bash, generateBashCompletion(data));
|
|
1054
|
+
// Use $HOME-relative path in source line (forward slashes)
|
|
1055
|
+
// Why: path.join() produces backslash paths on Windows, which MINGW64 bash can't interpret
|
|
1056
|
+
const bashRelPath = '.local/share/bash-completion/completions/claude-hooks';
|
|
1057
|
+
const bashSourceLine = `[ -f "$HOME/${bashRelPath}" ] && source "$HOME/${bashRelPath}"`;
|
|
1058
|
+
// Write to both .bashrc and .bash_profile for maximum compatibility
|
|
1059
|
+
// Why: MINGW64/Git Bash reads .bash_profile first; some setups don't source .bashrc from it
|
|
1060
|
+
const home = os.homedir();
|
|
1061
|
+
const bashrc = path.join(home, '.bashrc');
|
|
1062
|
+
appendLineIfMissing(bashrc, '# claude-hooks completions', bashSourceLine);
|
|
1063
|
+
const bashProfile = path.join(home, '.bash_profile');
|
|
1064
|
+
if (fs.existsSync(bashProfile)) {
|
|
1065
|
+
appendLineIfMissing(
|
|
1066
|
+
bashProfile,
|
|
1067
|
+
'# claude-hooks completions',
|
|
1068
|
+
bashSourceLine
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
installed++;
|
|
1072
|
+
} catch (e) {
|
|
1073
|
+
warning(`Bash completions: ${e.message}`);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// Zsh
|
|
1077
|
+
try {
|
|
1078
|
+
fs.mkdirSync(path.dirname(paths.zsh), { recursive: true });
|
|
1079
|
+
fs.writeFileSync(paths.zsh, generateZshCompletion(data));
|
|
1080
|
+
const zshrc = path.join(os.homedir(), '.zshrc');
|
|
1081
|
+
appendLineIfMissing(
|
|
1082
|
+
zshrc,
|
|
1083
|
+
'# claude-hooks completions',
|
|
1084
|
+
'fpath=(~/.zfunc $fpath); autoload -Uz compinit && compinit'
|
|
1085
|
+
);
|
|
1086
|
+
installed++;
|
|
1087
|
+
} catch (e) {
|
|
1088
|
+
warning(`Zsh completions: ${e.message}`);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// Fish
|
|
1092
|
+
try {
|
|
1093
|
+
fs.mkdirSync(path.dirname(paths.fish), { recursive: true });
|
|
1094
|
+
fs.writeFileSync(paths.fish, generateFishCompletion(data));
|
|
1095
|
+
installed++;
|
|
1096
|
+
} catch (e) {
|
|
1097
|
+
warning(`Fish completions: ${e.message}`);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// PowerShell
|
|
1101
|
+
try {
|
|
1102
|
+
fs.mkdirSync(path.dirname(paths.powershell), { recursive: true });
|
|
1103
|
+
fs.writeFileSync(paths.powershell, generatePowerShellCompletion(data));
|
|
1104
|
+
// Use $HOME-relative path with forward slashes (PowerShell supports both separators)
|
|
1105
|
+
const psRelPath = '.config/powershell/completions/claude-hooks.ps1';
|
|
1106
|
+
const psProfile = getPowerShellProfilePath();
|
|
1107
|
+
try {
|
|
1108
|
+
fs.mkdirSync(path.dirname(psProfile), { recursive: true });
|
|
1109
|
+
appendLineIfMissing(
|
|
1110
|
+
psProfile,
|
|
1111
|
+
'# claude-hooks completions',
|
|
1112
|
+
`. "$HOME/${psRelPath}"`
|
|
1113
|
+
);
|
|
1114
|
+
} catch (e) {
|
|
1115
|
+
warning(`PowerShell profile: ${e.message}`);
|
|
1116
|
+
}
|
|
1117
|
+
installed++;
|
|
1118
|
+
} catch (e) {
|
|
1119
|
+
warning(`PowerShell completions: ${e.message}`);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
if (installed > 0) {
|
|
1123
|
+
success(`Shell completions installed (${installed} shells)`);
|
|
1124
|
+
info('Restart your shell or open a new terminal for completions to take effect.');
|
|
1125
|
+
}
|
|
1126
|
+
} catch (e) {
|
|
1127
|
+
warning(`Shell completions: ${e.message}`);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* Remove shell completion scripts and rc file modifications
|
|
1133
|
+
* Called by runUninstall() for cleanup.
|
|
1134
|
+
*/
|
|
1135
|
+
export function removeCompletions() {
|
|
1136
|
+
const paths = getCompletionPaths();
|
|
1137
|
+
|
|
1138
|
+
// Remove completion files
|
|
1139
|
+
for (const [shell, filePath] of Object.entries(paths)) {
|
|
1140
|
+
try {
|
|
1141
|
+
if (fs.existsSync(filePath)) {
|
|
1142
|
+
fs.unlinkSync(filePath);
|
|
1143
|
+
}
|
|
1144
|
+
} catch (e) {
|
|
1145
|
+
warning(`Could not remove ${shell} completion: ${e.message}`);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// Clean rc files (bashrc, bash_profile, zshrc, PowerShell profile)
|
|
1150
|
+
const rcFiles = [
|
|
1151
|
+
path.join(os.homedir(), '.bashrc'),
|
|
1152
|
+
path.join(os.homedir(), '.bash_profile'),
|
|
1153
|
+
path.join(os.homedir(), '.zshrc'),
|
|
1154
|
+
getPowerShellProfilePath()
|
|
1155
|
+
];
|
|
1156
|
+
|
|
1157
|
+
for (const rcFile of rcFiles) {
|
|
1158
|
+
try {
|
|
1159
|
+
if (fs.existsSync(rcFile)) {
|
|
1160
|
+
let content = fs.readFileSync(rcFile, 'utf8');
|
|
1161
|
+
const marker = '# claude-hooks completions';
|
|
1162
|
+
if (content.includes(marker)) {
|
|
1163
|
+
content = content.replace(/\n?# claude-hooks completions\n[^\n]*\n?/g, '\n');
|
|
1164
|
+
fs.writeFileSync(rcFile, content);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
} catch (e) {
|
|
1168
|
+
warning(`Could not clean ${path.basename(rcFile)}: ${e.message}`);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|