Skip to main content

claude_hook_advisor/
cli.rs

1//! CLI interface and main entry point
2
3use crate::hooks::run_as_hook;
4use crate::Config;
5use anyhow::{Context, Result};
6use clap::{Arg, Command};
7use std::fs;
8use std::path::Path;
9
10/// Main entry point for the Claude Hook Advisor application.
11/// 
12/// Parses command-line arguments and dispatches to the appropriate mode:
13/// - `--hook`: Run as a Claude Code PreToolUse hook (reads JSON from stdin)
14/// - `--install`: Interactive installer to set up project configuration
15/// - Default: Show usage information
16pub fn run_cli() -> Result<()> {
17    let matches = Command::new("claude-hook-advisor")
18        .version(env!("CARGO_PKG_VERSION"))
19        .about("Advises Claude Code on better command alternatives based on project preferences")
20        .arg(
21            Arg::new("config")
22                .short('c')
23                .long("config")
24                .value_name("FILE")
25                .help("Path to configuration file")
26                .default_value(".claude-hook-advisor.toml"),
27        )
28        .arg(
29            Arg::new("hook")
30                .long("hook")
31                .help("Run as a Claude Code hook (reads JSON from stdin)")
32                .action(clap::ArgAction::SetTrue),
33        )
34        .arg(
35            Arg::new("replace")
36                .long("replace")
37                .help("Replace commands instead of blocking (experimental, not yet supported by Claude Code)")
38                .action(clap::ArgAction::SetTrue),
39        )
40        .arg(
41            Arg::new("install")
42                .long("install")
43                .help("Install Claude Hook Advisor: configure hooks and create/update config file")
44                .action(clap::ArgAction::SetTrue),
45        )
46        .arg(
47            Arg::new("uninstall")
48                .long("uninstall")
49                .help("Remove Claude Hook Advisor hooks from Claude Code settings")
50                .action(clap::ArgAction::SetTrue),
51        )
52        .arg(
53            Arg::new("history")
54                .long("history")
55                .help("View command history")
56                .action(clap::ArgAction::SetTrue),
57        )
58        .arg(
59            Arg::new("limit")
60                .long("limit")
61                .value_name("N")
62                .help("Limit number of history results (default: 20)")
63                .value_parser(clap::value_parser!(usize)),
64        )
65        .arg(
66            Arg::new("session")
67                .long("session")
68                .value_name("ID")
69                .help("Filter history by session ID"),
70        )
71        .arg(
72            Arg::new("failures")
73                .long("failures")
74                .help("Show only failed commands (non-zero exit codes)")
75                .action(clap::ArgAction::SetTrue),
76        )
77        .arg(
78            Arg::new("pattern")
79                .long("pattern")
80                .value_name("PATTERN")
81                .help("Filter commands by pattern (e.g., 'git', 'npm')"),
82        )
83        .get_matches();
84
85    let config_path = matches.get_one::<String>("config")
86        .expect("config argument has default value");
87    let replace_mode = matches.get_flag("replace");
88
89    if matches.get_flag("hook") {
90        run_as_hook(config_path, replace_mode)
91    } else if matches.get_flag("install") {
92        run_smart_installation(config_path)
93    } else if matches.get_flag("uninstall") {
94        crate::installer::uninstall_claude_hooks()
95    } else if matches.get_flag("history") {
96        let limit = matches.get_one::<usize>("limit").copied();
97        let session_id = matches.get_one::<String>("session").map(|s| s.to_string());
98        let failures_only = matches.get_flag("failures");
99        let pattern = matches.get_one::<String>("pattern").map(|s| s.to_string());
100
101        show_command_history(config_path, limit, session_id, failures_only, pattern)
102    } else {
103        println!("Claude Hook Advisor v{}", env!("CARGO_PKG_VERSION"));
104        println!();
105        println!("Installation:");
106        println!("  --install                 Install Claude Hook Advisor: configure hooks and create/update config file");
107        println!();
108        println!("Command Mapping:");
109        println!("  --hook                    Run as a Claude Code hook");
110        println!();
111        println!("Command History:");
112        println!("  --history                 View command history");
113        println!("  --limit <N>               Limit number of results (default: 20)");
114        println!("  --session <ID>            Filter by session ID");
115        println!("  --failures                Show only failed commands");
116        println!("  --pattern <PATTERN>       Filter by command pattern");
117        println!();
118        println!("Configuration:");
119        println!("  -c, --config <FILE>       Path to config file [default: .claude-hook-advisor.toml]");
120        println!();
121        println!("To configure directory aliases and command mappings, edit .claude-hook-advisor.toml directly.");
122        Ok(())
123    }
124}
125
126
127/// Shows command history from the SQLite database.
128///
129/// # Arguments
130/// * `config_path` - Path to configuration file (to get history DB path)
131/// * `limit` - Maximum number of records to show
132/// * `session_id` - Optional session ID filter
133/// * `failures_only` - Whether to show only failed commands
134/// * `pattern` - Optional command pattern filter
135///
136/// # Returns
137/// * `Ok(())` - History displayed successfully
138/// * `Err` - If database query fails
139fn show_command_history(
140    config_path: &str,
141    limit: Option<usize>,
142    session_id: Option<String>,
143    failures_only: bool,
144    pattern: Option<String>,
145) -> Result<()> {
146    use crate::history;
147
148    // Load config to get history database path
149    let config = crate::config::load_config(config_path)
150        .context("Failed to load configuration")?;
151
152    // Get history configuration
153    let history_config = match config.command_history {
154        Some(ref cfg) if cfg.enabled => cfg,
155        Some(_) => {
156            println!("Command history is disabled in configuration.");
157            return Ok(());
158        }
159        None => {
160            println!("Command history is not configured.");
161            println!("Add a [command_history] section to your .claude-hook-advisor.toml:");
162            println!();
163            println!("[command_history]");
164            println!("enabled = true");
165            println!("log_file = \"~/.claude-hook-advisor/bash-history.db\"");
166            return Ok(());
167        }
168    };
169
170    // Expand tilde in log file path
171    let log_path = expand_tilde_path(&history_config.log_file)?;
172
173    // Check if database file exists
174    if !log_path.exists() {
175        println!("No command history found at: {}", log_path.display());
176        println!("Commands will be logged once you start using Claude Code with hooks enabled.");
177        return Ok(());
178    }
179
180    // Open database connection
181    let conn = history::init_database(&log_path)
182        .context("Failed to open command history database")?;
183
184    // Build query
185    let query = history::HistoryQuery {
186        limit: Some(limit.unwrap_or(20)),
187        session_id,
188        failures_only,
189        command_pattern: pattern,
190    };
191
192    // Execute query
193    let records = history::query_history(&conn, &query)
194        .context("Failed to query command history")?;
195
196    if records.is_empty() {
197        println!("No commands found matching the specified criteria.");
198        return Ok(());
199    }
200
201    // Display results
202    println!("Command History ({} records)", records.len());
203    println!("{}", "=".repeat(80));
204    println!();
205
206    for record in records {
207        let timestamp = record.timestamp;
208        let exit_code_str = match record.exit_code {
209            Some(0) => "āœ“".to_string(),
210            Some(code) => format!("āœ— (exit: {})", code),
211            None => "?".to_string(),
212        };
213
214        println!("{}  {}", timestamp, exit_code_str);
215        println!("  Command: {}", record.command);
216
217        if let Some(cwd) = record.cwd {
218            println!("  CWD:     {}", cwd);
219        }
220
221        if record.was_replaced {
222            if let Some(original) = record.original_command {
223                println!("  Original: {}", original);
224            }
225        }
226
227        println!("  Session: {}", record.session_id);
228        println!();
229    }
230
231    Ok(())
232}
233
234/// Expands tilde (~) in file paths to the user's home directory
235fn expand_tilde_path(path: &str) -> Result<std::path::PathBuf> {
236    if path.starts_with("~/") {
237        let home = std::env::var("HOME")
238            .context("HOME environment variable not set")?;
239        Ok(std::path::PathBuf::from(path.replacen("~", &home, 1)))
240    } else {
241        Ok(std::path::PathBuf::from(path))
242    }
243}
244
245/// Smart installation that checks existing state and only makes necessary changes.
246/// 
247/// This function:
248/// 1. Checks if hooks already exist - if so, skips hook installation
249/// 2. Checks if config file exists - if not, creates it with examples
250/// 3. If config exists, ensures required sections exist with commented examples
251/// 
252/// # Arguments
253/// * `config_path` - Path to the configuration file
254/// 
255/// # Returns
256/// * `Ok(())` - Installation completed successfully
257/// * `Err` - If any installation step fails
258fn run_smart_installation(config_path: &str) -> Result<()> {
259    println!("šŸš€ Claude Hook Advisor Installation");
260    println!("===================================\n");
261    
262    // Step 1: Check and install hooks if needed
263    if hooks_already_exist()? {
264        println!("āœ… Hooks already installed in Claude Code settings");
265    } else {
266        println!("šŸ“‹ Installing hooks into Claude Code settings...");
267        crate::installer::install_claude_hooks()?;
268        println!("āœ… Hooks installed successfully");
269    }
270    
271    // Step 2: Handle config file
272    println!("\nšŸ“„ Checking configuration file...");
273    if Path::new(config_path).exists() {
274        println!("āœ… Config file exists: {config_path}");
275        ensure_config_sections(config_path)?;
276    } else {
277        println!("šŸ“ Creating new config file: {config_path}");
278        create_smart_config(config_path)?;
279    }
280    
281    println!("\nšŸŽ‰ Installation complete! Claude Hook Advisor is ready to use.");
282    println!("šŸ’” You can now use semantic directory references in Claude Code conversations.");
283    
284    Ok(())
285}
286
287/// Checks if Claude Hook Advisor hooks are already installed in Claude Code settings.
288/// 
289/// # Returns
290/// * `Ok(true)` - Hooks are already installed
291/// * `Ok(false)` - Hooks are not installed
292/// * `Err` - If settings file cannot be read or parsed
293fn hooks_already_exist() -> Result<bool> {
294    // Check for settings files in order of preference
295    let local_settings = Path::new(".claude/settings.local.json");
296    let shared_settings = Path::new(".claude/settings.json");
297    
298    let settings_path = if local_settings.exists() {
299        local_settings
300    } else if shared_settings.exists() {
301        shared_settings
302    } else {
303        return Ok(false); // No settings file means no hooks
304    };
305    
306    // Read and parse settings file
307    let settings_content = fs::read_to_string(settings_path)
308        .with_context(|| format!("Failed to read {}", settings_path.display()))?;
309    
310    let settings: serde_json::Value = serde_json::from_str(&settings_content)
311        .with_context(|| "Failed to parse Claude settings JSON")?;
312    
313    // Check if our hooks exist
314    if let Some(hooks) = settings.get("hooks").and_then(|h| h.as_object()) {
315        // Check PreToolUse and UserPromptSubmit hooks
316        for event_name in &["PreToolUse", "UserPromptSubmit"] {
317            if let Some(event_hooks) = hooks.get(*event_name).and_then(|h| h.as_array()) {
318                for hook_group in event_hooks {
319                    if let Some(hooks_array) = hook_group.get("hooks").and_then(|h| h.as_array()) {
320                        for hook in hooks_array {
321                            if let Some(command) = hook.get("command").and_then(|c| c.as_str()) {
322                                if command.contains("claude-hook-advisor") {
323                                    return Ok(true);
324                                }
325                            }
326                        }
327                    }
328                }
329            }
330        }
331    }
332    
333    Ok(false)
334}
335
336/// Creates a smart configuration file with project-specific command mappings.
337/// Detects the project type and generates appropriate command mappings.
338/// Directory aliases are provided as commented examples only.
339/// 
340/// # Arguments
341/// * `config_path` - Path where to create the configuration file
342/// 
343/// # Returns
344/// * `Ok(())` - Configuration created successfully
345/// * `Err` - If file writing fails
346fn create_smart_config(config_path: &str) -> Result<()> {
347    // Detect project type
348    let project_type = detect_project_type()?;
349    println!("šŸ” Detected project type: {project_type}");
350    
351    // Get project-specific command mappings
352    let commands = get_commands_for_project_type(&project_type);
353    
354    // Create config structure with actual commands but empty directories
355    let config = Config {
356        commands,
357        semantic_directories: std::collections::HashMap::new(), // Empty - will be comments only
358        command_history: None, // Will be added as commented example
359    };
360    
361    // Generate TOML content
362    let toml_content = toml::to_string_pretty(&config)
363        .with_context(|| "Failed to serialize configuration to TOML")?;
364    
365    // Build the complete config with header and directory examples as comments
366    let _project_name = get_project_name();
367    let final_content = format!(r#"# Claude Hook Advisor Configuration
368# Auto-generated for {project_type} project
369# This file configures command mappings and semantic directory aliases
370# for use with Claude Code integration.
371
372{toml_content}
373# Semantic directory aliases - natural language directory references
374# Uncomment and customize these examples:
375# docs = "~/Documents/Documentation"
376# central_docs = "~/Documents/Documentation"
377# project_docs = "~/Documents/Documentation/my-project"
378# claude_docs = "~/Documents/Documentation/claude"
379
380# Command history tracking - logs all Bash commands Claude runs
381# Uncomment to enable:
382# [command_history]
383# enabled = true
384# log_file = "~/.claude-hook-advisor/bash-history.db"
385"#);
386    
387    fs::write(config_path, final_content)
388        .with_context(|| format!("Failed to write config file: {config_path}"))?;
389    
390    println!("āœ… Created smart configuration for {project_type} project");
391    
392    // Show what was configured
393    if !config.commands.is_empty() {
394        println!("šŸ“ Command mappings configured:");
395        for (from, to) in &config.commands {
396            println!("   {from} → {to}");
397        }
398    } else {
399        println!("šŸ“ No specific command mappings for {project_type} - using general alternatives");
400    }
401    
402    Ok(())
403}
404
405/// Detects the project type by examining files in the current directory.
406/// 
407/// # Returns
408/// * `Ok(String)` - Detected project type ("Node.js", "Python", "Rust", etc.)
409/// * `Err` - If current directory cannot be accessed
410fn detect_project_type() -> Result<String> {
411    let current_dir = std::env::current_dir()?;
412
413    // Check for various project indicators
414    if current_dir.join("package.json").exists() {
415        return Ok("Node.js".to_string());
416    }
417
418    if current_dir.join("requirements.txt").exists()
419        || current_dir.join("pyproject.toml").exists()
420        || current_dir.join("setup.py").exists()
421    {
422        return Ok("Python".to_string());
423    }
424
425    if current_dir.join("Cargo.toml").exists() {
426        return Ok("Rust".to_string());
427    }
428
429    if current_dir.join("go.mod").exists() {
430        return Ok("Go".to_string());
431    }
432
433    if current_dir.join("pom.xml").exists() || current_dir.join("build.gradle").exists() {
434        return Ok("Java".to_string());
435    }
436
437    if current_dir.join("Dockerfile").exists() {
438        return Ok("Docker".to_string());
439    }
440
441    Ok("General".to_string())
442}
443
444/// Creates project-specific command mappings based on detected project type.
445/// 
446/// # Arguments
447/// * `project_type` - The detected project type
448/// 
449/// # Returns
450/// * `HashMap<String, String>` - Command mappings for the project
451fn get_commands_for_project_type(project_type: &str) -> std::collections::HashMap<String, String> {
452    let mut commands = std::collections::HashMap::new();
453    
454    match project_type {
455        "Node.js" => {
456            commands.insert("npm".to_string(), "bun".to_string());
457            commands.insert("yarn".to_string(), "bun".to_string());
458            commands.insert("pnpm".to_string(), "bun".to_string());
459            commands.insert("npx".to_string(), "bunx".to_string());
460            commands.insert("npm start".to_string(), "bun dev".to_string());
461            commands.insert("npm test".to_string(), "bun test".to_string());
462            commands.insert("npm run build".to_string(), "bun run build".to_string());
463        }
464        "Python" => {
465            commands.insert("pip".to_string(), "uv pip".to_string());
466            commands.insert("pip install".to_string(), "uv add".to_string());
467            commands.insert("pip uninstall".to_string(), "uv remove".to_string());
468            commands.insert("python".to_string(), "uv run python".to_string());
469            commands.insert("python -m".to_string(), "uv run python -m".to_string());
470        }
471        "Rust" => {
472            commands.insert("cargo check".to_string(), "cargo clippy".to_string());
473            commands.insert("cargo test".to_string(), "cargo test -- --nocapture".to_string());
474        }
475        "Go" => {
476            commands.insert("go run".to_string(), "go run -race".to_string());
477            commands.insert("go test".to_string(), "go test -v".to_string());
478        }
479        "Java" => {
480            commands.insert("mvn".to_string(), "./mvnw".to_string());
481            commands.insert("gradle".to_string(), "./gradlew".to_string());
482        }
483        "Docker" => {
484            commands.insert("docker".to_string(), "podman".to_string());
485            commands.insert("docker-compose".to_string(), "podman-compose".to_string());
486        }
487        _ => {
488            // General project - modern CLI alternatives
489            commands.insert("cat".to_string(), "bat".to_string());
490            commands.insert("ls".to_string(), "eza".to_string());
491            commands.insert("grep".to_string(), "rg".to_string());
492            commands.insert("find".to_string(), "fd".to_string());
493        }
494    }
495    
496    // Add common safety and modern tool mappings for all project types
497    commands.insert("curl".to_string(), "curl -L".to_string());
498    commands.insert("rm".to_string(), "trash".to_string());
499    commands.insert("rm -rf".to_string(), "echo 'Use trash command for safety'".to_string());
500    
501    commands
502}
503
504/// Gets the current project name for variable substitution.
505fn get_project_name() -> String {
506    std::env::current_dir()
507        .ok()
508        .and_then(|dir| dir.file_name().map(|name| name.to_string_lossy().to_string()))
509        .unwrap_or_else(|| "project".to_string())
510}
511
512
513/// Ensures required sections exist in an existing config file.
514/// 
515/// # Arguments
516/// * `config_path` - Path to the configuration file
517/// 
518/// # Returns
519/// * `Ok(())` - Configuration updated successfully
520/// * `Err` - If file operations fail
521fn ensure_config_sections(config_path: &str) -> Result<()> {
522    let mut config_content = fs::read_to_string(config_path)
523        .with_context(|| format!("Failed to read config file: {config_path}"))?;
524    
525    let mut needs_update = false;
526    
527    // Check and add missing sections
528    if !config_content.contains("[commands]") {
529        config_content.push_str("\n# Command mappings - suggest alternatives when Claude Code runs these commands\n");
530        config_content.push_str("[commands]\n");
531        config_content.push_str("# npm = \"bun\"          # Suggest 'bun' instead of 'npm'\n");
532        config_content.push_str("# yarn = \"bun\"         # Suggest 'bun' instead of 'yarn'\n");
533        config_content.push_str("# npx = \"bunx\"         # Suggest 'bunx' instead of 'npx'\n");
534        config_content.push_str("# grep = \"rg\"          # Suggest 'rg' (ripgrep) instead of 'grep'\n\n");
535        needs_update = true;
536        println!("āœ… Added [commands] section with examples");
537    }
538    
539    if !config_content.contains("[semantic_directories]") {
540        config_content.push_str("# Semantic directory aliases - natural language directory references\n");
541        config_content.push_str("[semantic_directories]\n");
542        config_content.push_str("docs = \"~/Documents/Documentation\"\n");
543        config_content.push_str("central_docs = \"~/Documents/Documentation\"\n");
544        config_content.push_str("project_docs = \"~/Documents/Documentation/my-project\"\n");
545        config_content.push_str("claude_docs = \"~/Documents/Documentation/claude\"\n\n");
546        needs_update = true;
547        println!("āœ… Added [semantic_directories] section with default aliases");
548    }
549    
550    
551    if needs_update {
552        fs::write(config_path, config_content)
553            .with_context(|| format!("Failed to update config file: {config_path}"))?;
554        println!("šŸ’¾ Configuration file updated");
555    } else {
556        println!("āœ… All required sections already present");
557    }
558    
559    Ok(())
560}
561
562
563#[cfg(test)]
564mod tests {
565    use super::*;
566    use tempfile::tempdir;
567    use serde_json::json;
568    
569    // Helper function to run a test in a temporary directory
570    fn with_temp_dir<F>(test: F) 
571    where 
572        F: FnOnce(),
573    {
574        let temp_dir = tempdir().unwrap();
575        let original_dir = std::env::current_dir().unwrap();
576        
577        // Change to temp directory
578        std::env::set_current_dir(temp_dir.path()).unwrap();
579        
580        // Run test with proper cleanup
581        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
582            test();
583        }));
584        
585        // Always restore original directory
586        std::env::set_current_dir(&original_dir).unwrap();
587        
588        // Re-panic if test panicked
589        if let Err(err) = result {
590            std::panic::resume_unwind(err);
591        }
592    }
593    
594    #[test]
595    fn test_hooks_already_exist_no_settings_file() {
596        with_temp_dir(|| {
597            let result = hooks_already_exist().unwrap();
598            assert!(!result, "Should return false when no settings files exist");
599        });
600    }
601    
602    #[test]
603    fn test_hooks_already_exist_empty_settings() {
604        with_temp_dir(|| {
605            // Create .claude directory and empty settings file
606            fs::create_dir_all(".claude").unwrap();
607            let settings_content = json!({});
608            fs::write(".claude/settings.local.json", serde_json::to_string_pretty(&settings_content).unwrap()).unwrap();
609            
610            let result = hooks_already_exist().unwrap();
611            assert!(!result, "Should return false when settings file has no hooks");
612        });
613    }
614    
615    #[test]
616    fn test_hooks_already_exist_with_our_hooks() {
617        with_temp_dir(|| {
618            // Create .claude directory and settings file with our hooks
619            fs::create_dir_all(".claude").unwrap();
620            let settings_content = json!({
621                "hooks": {
622                    "PreToolUse": [
623                        {
624                            "matcher": "Bash",
625                            "hooks": [
626                                {
627                                    "type": "command",
628                                    "command": "claude-hook-advisor --hook"
629                                }
630                            ]
631                        }
632                    ]
633                }
634            });
635            fs::write(".claude/settings.local.json", serde_json::to_string_pretty(&settings_content).unwrap()).unwrap();
636            
637            let result = hooks_already_exist().unwrap();
638            assert!(result, "Should return true when our hooks are present");
639        });
640    }
641    
642    #[test]
643    fn test_hooks_already_exist_with_other_hooks() {
644        with_temp_dir(|| {
645            // Create .claude directory and settings file with other hooks
646            fs::create_dir_all(".claude").unwrap();
647            let settings_content = json!({
648                "hooks": {
649                    "PreToolUse": [
650                        {
651                            "matcher": "Bash",
652                            "hooks": [
653                                {
654                                    "type": "command",
655                                    "command": "some-other-tool --hook"
656                                }
657                            ]
658                        }
659                    ]
660                }
661            });
662            fs::write(".claude/settings.local.json", serde_json::to_string_pretty(&settings_content).unwrap()).unwrap();
663            
664            let result = hooks_already_exist().unwrap();
665            assert!(!result, "Should return false when only other hooks are present");
666        });
667    }
668    
669    #[test]
670    fn test_hooks_already_exist_userprompsubmit_hooks() {
671        with_temp_dir(|| {
672            // Create .claude directory and settings file with UserPromptSubmit hooks
673            fs::create_dir_all(".claude").unwrap();
674            let settings_content = json!({
675                "hooks": {
676                    "UserPromptSubmit": [
677                        {
678                            "hooks": [
679                                {
680                                    "type": "command",
681                                    "command": "/path/to/claude-hook-advisor --hook"
682                                }
683                            ]
684                        }
685                    ]
686                }
687            });
688            fs::write(".claude/settings.local.json", serde_json::to_string_pretty(&settings_content).unwrap()).unwrap();
689            
690            let result = hooks_already_exist().unwrap();
691            assert!(result, "Should return true when UserPromptSubmit hooks are present");
692        });
693    }
694    
695    #[test]
696    fn test_hooks_already_exist_prefers_local_settings() {
697        with_temp_dir(|| {
698            // Create .claude directory
699            fs::create_dir_all(".claude").unwrap();
700            
701            // Create shared settings with our hooks
702            let shared_settings = json!({
703                "hooks": {
704                    "PreToolUse": [
705                        {
706                            "matcher": "Bash",
707                            "hooks": [
708                                {
709                                    "type": "command",
710                                    "command": "claude-hook-advisor --hook"
711                                }
712                            ]
713                        }
714                    ]
715                }
716            });
717            fs::write(".claude/settings.json", serde_json::to_string_pretty(&shared_settings).unwrap()).unwrap();
718            
719            // Create local settings without our hooks
720            let local_settings = json!({});
721            fs::write(".claude/settings.local.json", serde_json::to_string_pretty(&local_settings).unwrap()).unwrap();
722            
723            let result = hooks_already_exist().unwrap();
724            assert!(!result, "Should check local settings first and return false when they don't have our hooks");
725        });
726    }
727    
728    #[test] 
729    fn test_create_example_config() {
730        let temp_dir = tempdir().unwrap();
731        let config_path = temp_dir.path().join("test-config.toml");
732        
733        create_smart_config(config_path.to_str().unwrap()).unwrap();
734        
735        let content = fs::read_to_string(&config_path).unwrap();
736        
737        // Check that all required sections are present
738        assert!(content.contains("[commands]"));
739        assert!(content.contains("[semantic_directories]"));
740        
741        // Check that default aliases are present
742        assert!(content.contains("docs = \"~/Documents/Documentation\""));
743        assert!(content.contains("docs = \"~/Documents/Documentation\""));
744        
745        // Check that comments are present
746        assert!(content.contains("# Claude Hook Advisor Configuration"));
747        assert!(content.contains("# Uncomment and customize these examples:"));
748    }
749    
750    #[test]
751    fn test_ensure_config_sections_missing_sections() {
752        let temp_dir = tempdir().unwrap();
753        let config_path = temp_dir.path().join("test-config.toml");
754        
755        // Create minimal config missing sections
756        fs::write(&config_path, "# Minimal config\n").unwrap();
757        
758        ensure_config_sections(config_path.to_str().unwrap()).unwrap();
759        
760        let content = fs::read_to_string(&config_path).unwrap();
761        
762        // Check that all sections were added
763        assert!(content.contains("[commands]"));
764        assert!(content.contains("[semantic_directories]"));
765        
766        // Check that examples were added
767        assert!(content.contains("docs = \"~/Documents/Documentation\""));
768        assert!(content.contains("# npm = \"bun\""));
769    }
770    
771    #[test]
772    fn test_ensure_config_sections_all_sections_present() {
773        let temp_dir = tempdir().unwrap();
774        let config_path = temp_dir.path().join("test-config.toml");
775        
776        let existing_config = r#"# Existing config
777[commands]
778npm = "bun"
779
780[semantic_directories]
781docs = "~/Documents"
782"#;
783        fs::write(&config_path, existing_config).unwrap();
784        
785        ensure_config_sections(config_path.to_str().unwrap()).unwrap();
786        
787        let content = fs::read_to_string(&config_path).unwrap();
788        
789        // Should be unchanged since all sections already exist
790        assert_eq!(content, existing_config);
791    }
792}