From ffdcc81060903782a6054dba9f2592adec075be5 Mon Sep 17 00:00:00 2001 From: Joseph Romero Date: Fri, 12 Dec 2025 11:04:25 -0500 Subject: [PATCH] Adds config option to disable auto edits of gitignore. --- README.md | 13 ++++++- src/cli/args.rs | 1 + src/cli/config_resolution.rs | 3 ++ src/cli/tests.rs | 67 ++++++++++++++++++++++++++++++++++ src/commands/clean.rs | 2 + src/commands/generate.rs | 71 ++++++++++++++++++++++++++++++++---- src/commands/mod.rs | 2 + src/commands/status.rs | 3 ++ src/config.rs | 48 ++++++++++++++++++++++++ 9 files changed, 202 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 46bc942..6e1a542 100644 --- a/README.md +++ b/README.md @@ -91,15 +91,26 @@ Create `ai-rules/ai-rules-config.yaml` in the `ai-rules` directory. Example: agents: [claude, cursor, cline] # Generate rules only for these agents nested_depth: 2 # Search 2 levels deep for ai-rules/ folders gitignore: true # Ignore the generated rules in git +auto_update_gitignore: true # Allow ai-rules to modify .gitignore (default: true) ``` +### Configuration Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `agents` | list | all agents | List of agents to generate rules for | +| `nested_depth` | number | 0 | Directory depth to scan for `ai-rules/` folders | +| `gitignore` | boolean | false | Add generated files to `.gitignore` | +| `auto_update_gitignore` | boolean | true | Allow ai-rules to modify `.gitignore`. Set to `false` to prevent any `.gitignore` changes (useful for repositories with strict `.gitignore` policies) | +| `use_claude_skills` | boolean | false | Enable Claude Code skills mode (experimental) | + ### Configuration Precedence Options are resolved in the following order (highest to lowest priority): 1. **CLI options** - `--agents`, `--nested-depth`, `--no-gitignore` 2. **Config file** - `ai-rules/ai-rules-config.yaml` (at current working directory) -3. **Default values** - All agents, depth 0, generated files are NOT git ignored +3. **Default values** - All agents, depth 0, generated files are NOT git ignored, auto-update of `.gitignore` enabled ### Experimental Options diff --git a/src/cli/args.rs b/src/cli/args.rs index 689864e..9d4d58c 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -118,6 +118,7 @@ pub struct ResolvedGenerateArgs { pub agents: Option>, pub gitignore: bool, pub nested_depth: usize, + pub auto_update_gitignore: bool, } #[derive(Debug)] diff --git a/src/cli/config_resolution.rs b/src/cli/config_resolution.rs index 0dc8695..432f8ee 100644 --- a/src/cli/config_resolution.rs +++ b/src/cli/config_resolution.rs @@ -46,10 +46,13 @@ impl GenerateArgs { false }; + let auto_update_gitignore = config.and_then(|c| c.auto_update_gitignore).unwrap_or(true); + ResolvedGenerateArgs { agents, gitignore, nested_depth: nested_depth.unwrap_or(0), + auto_update_gitignore, } } } diff --git a/src/cli/tests.rs b/src/cli/tests.rs index b45232a..b5007f7 100644 --- a/src/cli/tests.rs +++ b/src/cli/tests.rs @@ -11,6 +11,7 @@ fn test_generate_args_with_config_cli_priority() { no_gitignore: None, nested_depth: Some(5), use_claude_skills: None, + auto_update_gitignore: None, }; let args = GenerateArgs { @@ -35,6 +36,7 @@ fn test_generate_args_with_config_uses_config_when_cli_missing() { no_gitignore: None, nested_depth: Some(3), use_claude_skills: None, + auto_update_gitignore: None, }; let args = GenerateArgs { @@ -75,6 +77,7 @@ fn test_generate_args_with_config_partial_config() { no_gitignore: None, nested_depth: None, use_claude_skills: None, + auto_update_gitignore: None, }; let args = GenerateArgs { @@ -99,6 +102,7 @@ fn test_nested_depth_args_with_config() { no_gitignore: None, nested_depth: Some(4), use_claude_skills: None, + auto_update_gitignore: None, }; let args_with_cli = NestedDepthArgs { @@ -121,6 +125,7 @@ fn test_nested_depth_explicit_zero_overrides_config() { no_gitignore: None, nested_depth: Some(5), use_claude_skills: None, + auto_update_gitignore: None, }; let args = NestedDepthArgs { @@ -138,6 +143,7 @@ fn test_status_args_with_config_cli_priority() { no_gitignore: None, nested_depth: Some(5), use_claude_skills: None, + auto_update_gitignore: None, }; let args = StatusArgs { @@ -161,6 +167,7 @@ fn test_status_args_with_config_uses_config_when_cli_missing() { no_gitignore: None, nested_depth: Some(3), use_claude_skills: None, + auto_update_gitignore: None, }; let args = StatusArgs { @@ -195,6 +202,7 @@ fn test_generate_args_backward_compat_no_gitignore_config() { no_gitignore: Some(true), nested_depth: None, use_claude_skills: None, + auto_update_gitignore: None, }; let args = GenerateArgs { @@ -217,6 +225,7 @@ fn test_generate_args_backward_compat_no_gitignore_cli() { no_gitignore: None, nested_depth: None, use_claude_skills: None, + auto_update_gitignore: None, }; let args = GenerateArgs { @@ -239,6 +248,7 @@ fn test_generate_args_new_gitignore_flag_overrides_old() { no_gitignore: None, nested_depth: None, use_claude_skills: None, + auto_update_gitignore: None, }; let args = GenerateArgs { @@ -252,3 +262,60 @@ fn test_generate_args_new_gitignore_flag_overrides_old() { assert!(resolved.gitignore); } + +#[test] +fn test_generate_args_auto_update_gitignore_defaults_to_true() { + let args = GenerateArgs { + agents: None, + gitignore: false, + no_gitignore: false, + nested_depth: None, + }; + + let resolved = args.with_config(None); + assert!(resolved.auto_update_gitignore); +} + +#[test] +fn test_generate_args_auto_update_gitignore_from_config_true() { + let config = config::Config { + agents: None, + gitignore: None, + no_gitignore: None, + nested_depth: None, + use_claude_skills: None, + auto_update_gitignore: Some(true), + }; + + let args = GenerateArgs { + agents: None, + gitignore: false, + no_gitignore: false, + nested_depth: None, + }; + + let resolved = args.with_config(Some(&config)); + assert!(resolved.auto_update_gitignore); +} + +#[test] +fn test_generate_args_auto_update_gitignore_from_config_false() { + let config = config::Config { + agents: None, + gitignore: None, + no_gitignore: None, + nested_depth: None, + use_claude_skills: None, + auto_update_gitignore: Some(false), + }; + + let args = GenerateArgs { + agents: None, + gitignore: false, + no_gitignore: false, + nested_depth: None, + }; + + let resolved = args.with_config(Some(&config)); + assert!(!resolved.auto_update_gitignore); +} diff --git a/src/commands/clean.rs b/src/commands/clean.rs index c996c79..9bc5176 100644 --- a/src/commands/clean.rs +++ b/src/commands/clean.rs @@ -160,6 +160,7 @@ Test rule content"#; agents: None, gitignore: false, nested_depth: 2, + auto_update_gitignore: true, }, false, ); @@ -209,6 +210,7 @@ Test rule content"#; ]), gitignore: false, nested_depth: CLEAN_NESTED_DEPTH, + auto_update_gitignore: true, }, false, ); diff --git a/src/commands/generate.rs b/src/commands/generate.rs index 594d0ec..45bae17 100644 --- a/src/commands/generate.rs +++ b/src/commands/generate.rs @@ -14,13 +14,14 @@ pub fn run_generate( use_claude_skills: bool, ) -> Result<()> { println!( - "Generating rules for agents: {}, nested_depth: {}, gitignore: {}", + "Generating rules for agents: {}, nested_depth: {}, gitignore: {}, auto_update_gitignore: {}", args.agents .as_ref() .map(|a| a.join(",")) .unwrap_or_else(|| "all".to_string()), args.nested_depth, - args.gitignore + args.gitignore, + args.auto_update_gitignore ); let registry = AgentToolRegistry::new(use_claude_skills); let agents = args.agents.unwrap_or_else(|| registry.get_all_tool_names()); @@ -32,11 +33,13 @@ pub fn run_generate( generation_result.display(current_dir); - if args.gitignore { - operations::update_project_gitignore(current_dir, ®istry, args.nested_depth)?; - print_success("Updated .gitignore with generated file patterns"); - } else { - operations::remove_gitignore_section(current_dir, ®istry)?; + if args.auto_update_gitignore { + if args.gitignore { + operations::update_project_gitignore(current_dir, ®istry, args.nested_depth)?; + print_success("Updated .gitignore with generated file patterns"); + } else { + operations::remove_gitignore_section(current_dir, ®istry)?; + } } Ok(()) @@ -151,6 +154,7 @@ mod tests { agents: None, gitignore: true, nested_depth: NESTED_DEPTH, + auto_update_gitignore: true, }; const TEST_RULE_CONTENT: &str = r#"--- @@ -245,6 +249,7 @@ Test rule content agents: None, gitignore: false, nested_depth: NESTED_DEPTH, + auto_update_gitignore: true, }; let result = run_generate(temp_dir.path(), args, false); assert!(result.is_ok()); @@ -267,6 +272,7 @@ Test rule content agents: Some(vec!["claude".to_string(), "cursor".to_string()]), gitignore: true, nested_depth: NESTED_DEPTH, + auto_update_gitignore: true, }; let result = run_generate(temp_dir.path(), args, false); assert!(result.is_ok()); @@ -378,6 +384,7 @@ Test rule content agents: Some(vec!["claude".to_string()]), gitignore: true, nested_depth: 0, + auto_update_gitignore: true, }; let result = run_generate(temp_dir.path(), args, false); assert!(result.is_ok()); @@ -419,6 +426,7 @@ Test rule content agents: Some(vec!["claude".to_string()]), gitignore: false, nested_depth: NESTED_DEPTH, + auto_update_gitignore: true, }; let result = run_generate(temp_dir.path(), args, false); assert!(result.is_ok()); @@ -623,6 +631,7 @@ Optional content"#, agents: Some(vec!["claude".to_string()]), gitignore: false, nested_depth: NESTED_DEPTH, + auto_update_gitignore: true, }; run_generate(temp_dir.path(), args.clone(), true).unwrap(); @@ -664,6 +673,7 @@ Optional content"#, ]), gitignore: false, nested_depth: NESTED_DEPTH, + auto_update_gitignore: true, }; let result = run_generate(temp_dir.path(), args, false); assert!(result.is_ok()); @@ -699,6 +709,7 @@ Optional content"#, agents: Some(vec!["claude".to_string(), "cursor".to_string()]), gitignore: false, nested_depth: NESTED_DEPTH, + auto_update_gitignore: true, }; let result = run_generate(temp_dir.path(), args, false); assert!(result.is_ok()); @@ -723,6 +734,7 @@ Optional content"#, agents: Some(vec!["firebender".to_string()]), gitignore: false, nested_depth: NESTED_DEPTH, + auto_update_gitignore: true, }; let result = run_generate(temp_dir.path(), args, false); assert!(result.is_ok()); @@ -731,4 +743,49 @@ Optional content"#, assert_file_not_exists(temp_dir.path(), ".mcp.json"); } + + #[test] + fn test_run_generate_with_auto_update_gitignore_false() { + let temp_dir = TempDir::new().unwrap(); + + // Create an existing .gitignore with ai-rules section + create_file( + temp_dir.path(), + ".gitignore", + r#"# Existing content +*.old + +# AI Rules - Generated Files +CLAUDE.md +# End AI Rules +"#, + ); + + create_file(temp_dir.path(), "ai-rules/test.md", TEST_RULE_CONTENT); + + let args = ResolvedGenerateArgs { + agents: Some(vec!["claude".to_string()]), + gitignore: true, // Would normally update gitignore + nested_depth: NESTED_DEPTH, + auto_update_gitignore: false, // But this prevents any changes + }; + let result = run_generate(temp_dir.path(), args, false); + assert!(result.is_ok()); + + // Generated files should still be created + assert_file_exists(temp_dir.path(), "CLAUDE.md"); + + // But .gitignore should be UNCHANGED + assert_file_content( + temp_dir.path(), + ".gitignore", + r#"# Existing content +*.old + +# AI Rules - Generated Files +CLAUDE.md +# End AI Rules +"#, + ); + } } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 6b97d18..ef1f5a5 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -70,6 +70,7 @@ mod tests { agents: None, gitignore: true, nested_depth, + auto_update_gitignore: true, }; let generate_result = run_generate(project_path, generate_args, false); if let Err(e) = &generate_result { @@ -148,6 +149,7 @@ mod tests { agents: None, gitignore: true, nested_depth, + auto_update_gitignore: true, }; let generate_result = run_generate(project_path, generate_args, false); assert!(generate_result.is_ok()); diff --git a/src/commands/status.rs b/src/commands/status.rs index e355307..71a006d 100644 --- a/src/commands/status.rs +++ b/src/commands/status.rs @@ -401,6 +401,7 @@ Test rule content"#; agents: None, gitignore: false, nested_depth, + auto_update_gitignore: true, }, false, ) @@ -539,6 +540,7 @@ Test rule content"#; agents: Some(vec!["claude".to_string()]), gitignore: false, nested_depth: NESTED_DEPTH, + auto_update_gitignore: true, }, false, ); @@ -656,6 +658,7 @@ Test command body"#; agents: Some(vec!["claude".to_string()]), gitignore: false, nested_depth: NESTED_DEPTH, + auto_update_gitignore: true, }, false, ); diff --git a/src/config.rs b/src/config.rs index 5a24bab..52490c2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,6 +10,7 @@ pub struct Config { pub no_gitignore: Option, pub nested_depth: Option, pub use_claude_skills: Option, + pub auto_update_gitignore: Option, } pub fn load_config(current_dir: &Path) -> Result> { @@ -166,4 +167,51 @@ nested_depth: 2 // New field should be None if not specified assert_eq!(config.gitignore, None); } + + #[test] + fn test_load_config_with_auto_update_gitignore_true() { + let temp_dir = TempDir::new().unwrap(); + let config_content = r#" +agents: ["claude"] +auto_update_gitignore: true +"#; + create_config_file(temp_dir.path(), config_content); + + let result = load_config(temp_dir.path()).unwrap(); + assert!(result.is_some()); + let config = result.unwrap(); + + assert_eq!(config.auto_update_gitignore, Some(true)); + } + + #[test] + fn test_load_config_with_auto_update_gitignore_false() { + let temp_dir = TempDir::new().unwrap(); + let config_content = r#" +agents: ["claude"] +auto_update_gitignore: false +"#; + create_config_file(temp_dir.path(), config_content); + + let result = load_config(temp_dir.path()).unwrap(); + assert!(result.is_some()); + let config = result.unwrap(); + + assert_eq!(config.auto_update_gitignore, Some(false)); + } + + #[test] + fn test_load_config_without_auto_update_gitignore_defaults_to_none() { + let temp_dir = TempDir::new().unwrap(); + let config_content = r#" +agents: ["claude"] +"#; + create_config_file(temp_dir.path(), config_content); + + let result = load_config(temp_dir.path()).unwrap(); + assert!(result.is_some()); + let config = result.unwrap(); + + assert!(config.auto_update_gitignore.is_none()); + } }