feat(cli): add configuration and hooks management commands
- Add --config flag to show current configuration and search paths - Add --create-config flag to generate example .shush.toml files - Add hook management flags: --install-hook, --uninstall-hook, --list-hooks, --hook-status - Implement cross-scope conflict detection with clear error messages - Add -s flag for hook scope selection (user-wide vs project-specific)
This commit is contained in:
+292
-1
@@ -5,6 +5,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/alecthomas/kong"
|
"github.com/alecthomas/kong"
|
||||||
|
"github.com/carlosarraes/shush/internal/config"
|
||||||
|
"github.com/carlosarraes/shush/internal/hooks"
|
||||||
"github.com/carlosarraes/shush/internal/processor"
|
"github.com/carlosarraes/shush/internal/processor"
|
||||||
"github.com/carlosarraes/shush/internal/types"
|
"github.com/carlosarraes/shush/internal/types"
|
||||||
)
|
)
|
||||||
@@ -23,6 +25,48 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
hookCommands := []bool{
|
||||||
|
cli.InstallHook,
|
||||||
|
cli.UninstallHook,
|
||||||
|
cli.ListHooks,
|
||||||
|
cli.HookStatus,
|
||||||
|
cli.Config,
|
||||||
|
cli.CreateConfig,
|
||||||
|
}
|
||||||
|
hookFlagCount := 0
|
||||||
|
for _, flag := range hookCommands {
|
||||||
|
if flag {
|
||||||
|
hookFlagCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hookFlagCount > 1 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: utility commands (--install-hook, --uninstall-hook, --list-hooks, --hook-status, --config, --create-config) are mutually exclusive\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hookFlagCount > 0 {
|
||||||
|
|
||||||
|
if cli.Recursive || cli.Inline || cli.Block || cli.DryRun || cli.Backup || cli.Verbose {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: hook commands cannot be combined with processing flags\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
gitFlags := []bool{cli.ChangesOnly, cli.Staged, cli.Unstaged}
|
||||||
|
for _, flag := range gitFlags {
|
||||||
|
if flag {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: hook commands cannot be combined with git flags\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := handleHookCommands(cli); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
gitFlags := []bool{cli.ChangesOnly, cli.Staged, cli.Unstaged}
|
gitFlags := []bool{cli.ChangesOnly, cli.Staged, cli.Unstaged}
|
||||||
gitFlagCount := 0
|
gitFlagCount := 0
|
||||||
for _, flag := range gitFlags {
|
for _, flag := range gitFlags {
|
||||||
@@ -46,7 +90,6 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if gitFlagCount == 0 && cli.Path == "" {
|
if gitFlagCount == 0 && cli.Path == "" {
|
||||||
fmt.Fprintf(os.Stderr, "Error: path argument is required\n")
|
fmt.Fprintf(os.Stderr, "Error: path argument is required\n")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -251,3 +294,251 @@ shush --staged --backup --verbose # Process with maximum safety
|
|||||||
Shush excels at safe, fast comment removal with excellent preview capabilities for confident code processing.
|
Shush excels at safe, fast comment removal with excellent preview capabilities for confident code processing.
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleHookCommands(cli types.CLI) error {
|
||||||
|
switch {
|
||||||
|
case cli.InstallHook:
|
||||||
|
return installHooks(cli.HookScope)
|
||||||
|
case cli.UninstallHook:
|
||||||
|
return uninstallHooks(cli.HookScope)
|
||||||
|
case cli.ListHooks:
|
||||||
|
return listHooks()
|
||||||
|
case cli.HookStatus:
|
||||||
|
return showHooksStatus()
|
||||||
|
case cli.Config:
|
||||||
|
return showConfig()
|
||||||
|
case cli.CreateConfig:
|
||||||
|
return createConfig()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func installHooks(scope string) error {
|
||||||
|
hookScope := hooks.ScopeUser
|
||||||
|
if scope == "project" {
|
||||||
|
hookScope = hooks.ScopeProject
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
userPath, _ := hooks.GetSettingsPath(hooks.ScopeUser)
|
||||||
|
projectPath, _ := hooks.GetSettingsPath(hooks.ScopeProject)
|
||||||
|
|
||||||
|
userSettings, userErr := hooks.LoadSettings(userPath)
|
||||||
|
projectSettings, projectErr := hooks.LoadSettings(projectPath)
|
||||||
|
|
||||||
|
userHasShush := userErr == nil && hooks.HasShushHook(userSettings)
|
||||||
|
projectHasShush := projectErr == nil && hooks.HasShushHook(projectSettings)
|
||||||
|
|
||||||
|
if hookScope == hooks.ScopeUser {
|
||||||
|
|
||||||
|
if userHasShush {
|
||||||
|
return fmt.Errorf("shush hook already installed for user-wide scope at %s", userPath)
|
||||||
|
}
|
||||||
|
if projectHasShush {
|
||||||
|
fmt.Printf("ℹ Project-specific hooks found at %s\n", projectPath)
|
||||||
|
fmt.Println(" User-wide hooks will take precedence and override project hooks")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if userHasShush {
|
||||||
|
return fmt.Errorf("user-wide hooks already installed at %s\n" +
|
||||||
|
" User-wide hooks cover all projects including this one.\n" +
|
||||||
|
" Use --uninstall-hook first if you want project-specific hooks instead", userPath)
|
||||||
|
}
|
||||||
|
if projectHasShush {
|
||||||
|
return fmt.Errorf("shush hook already installed for project scope at %s", projectPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := hooks.GetSettingsPath(hookScope)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get settings path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
settings, err := hooks.LoadSettings(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := hooks.AddShushHook(settings); err != nil {
|
||||||
|
return fmt.Errorf("failed to add shush hook: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := hooks.SaveSettings(path, settings); err != nil {
|
||||||
|
return fmt.Errorf("failed to save settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scopeName := "user-wide"
|
||||||
|
if hookScope == hooks.ScopeProject {
|
||||||
|
scopeName = "project"
|
||||||
|
}
|
||||||
|
fmt.Printf("✓ Hooks installed for %s scope at %s\n", scopeName, path)
|
||||||
|
fmt.Println(" Auto-cleanup will run after Claude modifies files")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func uninstallHooks(scope string) error {
|
||||||
|
hookScope := hooks.ScopeUser
|
||||||
|
if scope == "project" {
|
||||||
|
hookScope = hooks.ScopeProject
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := hooks.GetSettingsPath(hookScope)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get settings path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
settings, err := hooks.LoadSettings(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hooks.HasShushHook(settings) {
|
||||||
|
scopeName := "user-wide"
|
||||||
|
if hookScope == hooks.ScopeProject {
|
||||||
|
scopeName = "project"
|
||||||
|
}
|
||||||
|
return fmt.Errorf("shush hook not found for %s scope at %s", scopeName, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := hooks.RemoveShushHook(settings); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove shush hook: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := hooks.SaveSettings(path, settings); err != nil {
|
||||||
|
return fmt.Errorf("failed to save settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scopeName := "user-wide"
|
||||||
|
if hookScope == hooks.ScopeProject {
|
||||||
|
scopeName = "project"
|
||||||
|
}
|
||||||
|
fmt.Printf("✓ Hooks uninstalled for %s scope at %s\n", scopeName, path)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func showHooksStatus() error {
|
||||||
|
userPath, _ := hooks.GetSettingsPath(hooks.ScopeUser)
|
||||||
|
projectPath, _ := hooks.GetSettingsPath(hooks.ScopeProject)
|
||||||
|
|
||||||
|
userSettings, userErr := hooks.LoadSettings(userPath)
|
||||||
|
projectSettings, projectErr := hooks.LoadSettings(projectPath)
|
||||||
|
|
||||||
|
userHasShush := userErr == nil && hooks.HasShushHook(userSettings)
|
||||||
|
projectHasShush := projectErr == nil && hooks.HasShushHook(projectSettings)
|
||||||
|
|
||||||
|
fmt.Println("Shush Hooks Status:")
|
||||||
|
fmt.Printf("User-wide (%s): %s\n", userPath, getHookStatus(userSettings, userErr))
|
||||||
|
fmt.Printf("Project (%s): %s\n", projectPath, getHookStatus(projectSettings, projectErr))
|
||||||
|
|
||||||
|
|
||||||
|
if userHasShush && projectHasShush {
|
||||||
|
fmt.Println("\n⚠️ Warning: Both user-wide and project hooks are installed")
|
||||||
|
fmt.Println(" This will cause shush to run twice on every file modification")
|
||||||
|
fmt.Println(" Consider removing project hooks: shush --uninstall-hook -s project")
|
||||||
|
} else if userHasShush {
|
||||||
|
fmt.Println("\n✓ User-wide hooks will handle all projects including this one")
|
||||||
|
} else if projectHasShush {
|
||||||
|
fmt.Println("\n✓ Project-specific hooks active for this project only")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHookStatus(settings *hooks.ClaudeSettings, err error) string {
|
||||||
|
if err != nil {
|
||||||
|
return "Not configured"
|
||||||
|
}
|
||||||
|
if hooks.HasShushHook(settings) {
|
||||||
|
return "✓ Installed"
|
||||||
|
}
|
||||||
|
return "Not installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
func listHooks() error {
|
||||||
|
userPath, _ := hooks.GetSettingsPath(hooks.ScopeUser)
|
||||||
|
projectPath, _ := hooks.GetSettingsPath(hooks.ScopeProject)
|
||||||
|
|
||||||
|
fmt.Println("Claude Code Hooks Configuration:")
|
||||||
|
|
||||||
|
fmt.Println("User-wide:")
|
||||||
|
if err := listHooksForPath(userPath); err != nil {
|
||||||
|
fmt.Println(" No hooks configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Project:")
|
||||||
|
if err := listHooksForPath(projectPath); err != nil {
|
||||||
|
fmt.Println(" No hooks configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func listHooksForPath(path string) error {
|
||||||
|
settings, err := hooks.LoadSettings(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.Hooks == nil || len(settings.Hooks) == 0 {
|
||||||
|
return fmt.Errorf("no hooks")
|
||||||
|
}
|
||||||
|
|
||||||
|
hasShush := hooks.HasShushHook(settings)
|
||||||
|
|
||||||
|
for event, configs := range settings.Hooks {
|
||||||
|
for _, config := range configs {
|
||||||
|
for _, hook := range config.Hooks {
|
||||||
|
if hook.Command == "shush --changes-only" {
|
||||||
|
fmt.Printf(" ✓ shush --changes-only (%s: %s)\n", event, config.Matcher)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" ✓ %s (%s: %s)\n", hook.Command, event, config.Matcher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasShush {
|
||||||
|
fmt.Println(" (No shush hooks found)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func showConfig() error {
|
||||||
|
cfg, configPath, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Shush Configuration:")
|
||||||
|
if configPath != "" {
|
||||||
|
fmt.Printf("Config file: %s\n", configPath)
|
||||||
|
} else {
|
||||||
|
fmt.Println("Config file: Using defaults (no config file found)")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nPreserve patterns (%d):\n", len(cfg.Preserve))
|
||||||
|
for i, pattern := range cfg.Preserve {
|
||||||
|
fmt.Printf(" %2d. %s\n", i+1, pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nConfig file search order:")
|
||||||
|
fmt.Println(" 1. .shush.toml (current directory)")
|
||||||
|
fmt.Println(" 2. .shush.toml (git repository root)")
|
||||||
|
fmt.Println(" 3. ~/.config/.shush.toml (global)")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createConfig() error {
|
||||||
|
if err := config.CreateExampleConfig(); err != nil {
|
||||||
|
return fmt.Errorf("failed to create config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("✓ Created .shush.toml configuration file")
|
||||||
|
fmt.Println(" Edit this file to customize which comments to preserve")
|
||||||
|
fmt.Println(" Patterns support wildcards with * (e.g., '*IMPORTANT*')")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
+19
-12
@@ -3,18 +3,25 @@ package types
|
|||||||
import "github.com/alecthomas/kong"
|
import "github.com/alecthomas/kong"
|
||||||
|
|
||||||
type CLI struct {
|
type CLI struct {
|
||||||
Path string `arg:"" name:"path" help:"Source code file or directory to process" optional:""`
|
Path string `arg:"" name:"path" help:"Source code file or directory to process" optional:""`
|
||||||
Inline bool `help:"Remove only line comments"`
|
Inline bool `help:"Remove only line comments"`
|
||||||
Block bool `help:"Remove only block comments"`
|
Block bool `help:"Remove only block comments"`
|
||||||
Recursive bool `short:"r" help:"Process directories recursively"`
|
Recursive bool `short:"r" help:"Process directories recursively"`
|
||||||
DryRun bool `help:"Show what would be removed without making changes"`
|
DryRun bool `help:"Show what would be removed without making changes"`
|
||||||
Backup bool `help:"Create backup files before modification"`
|
Backup bool `help:"Create backup files before modification"`
|
||||||
Verbose bool `help:"Show detailed output"`
|
Verbose bool `help:"Show detailed output"`
|
||||||
LLM bool `help:"Show LLM-friendly usage guide"`
|
LLM bool `help:"Show LLM-friendly usage guide"`
|
||||||
ChangesOnly bool `help:"Remove comments only from git changes (staged + unstaged + untracked)"`
|
ChangesOnly bool `help:"Remove comments only from git changes (staged + unstaged + untracked)"`
|
||||||
Staged bool `help:"Remove comments only from staged git changes"`
|
Staged bool `help:"Remove comments only from staged git changes"`
|
||||||
Unstaged bool `help:"Remove comments only from unstaged git changes"`
|
Unstaged bool `help:"Remove comments only from unstaged git changes"`
|
||||||
Version kong.VersionFlag `help:"Show version information"`
|
InstallHook bool `help:"Install Claude Code hooks for automatic comment cleanup"`
|
||||||
|
UninstallHook bool `help:"Uninstall Claude Code hooks"`
|
||||||
|
ListHooks bool `help:"List current Claude Code hooks configuration"`
|
||||||
|
HookStatus bool `help:"Check if shush hooks are installed"`
|
||||||
|
HookScope string `short:"s" help:"Hook scope: 'project' for local, default for user-wide"`
|
||||||
|
Config bool `help:"Show current configuration and location"`
|
||||||
|
CreateConfig bool `help:"Create example .shush.toml configuration file"`
|
||||||
|
Version kong.VersionFlag `help:"Show version information"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Language struct {
|
type Language struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user