Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions src/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ pub struct ResolvedGenerateArgs {
pub agents: Option<Vec<String>>,
pub gitignore: bool,
pub nested_depth: usize,
pub auto_update_gitignore: bool,
}

#[derive(Debug)]
Expand Down
3 changes: 3 additions & 0 deletions src/cli/config_resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
}
Expand Down
67 changes: 67 additions & 0 deletions src/cli/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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);
}
2 changes: 2 additions & 0 deletions src/commands/clean.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ Test rule content"#;
agents: None,
gitignore: false,
nested_depth: 2,
auto_update_gitignore: true,
},
false,
);
Expand Down Expand Up @@ -209,6 +210,7 @@ Test rule content"#;
]),
gitignore: false,
nested_depth: CLEAN_NESTED_DEPTH,
auto_update_gitignore: true,
},
false,
);
Expand Down
71 changes: 64 additions & 7 deletions src/commands/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -32,11 +33,13 @@ pub fn run_generate(

generation_result.display(current_dir);

if args.gitignore {
operations::update_project_gitignore(current_dir, &registry, args.nested_depth)?;
print_success("Updated .gitignore with generated file patterns");
} else {
operations::remove_gitignore_section(current_dir, &registry)?;
if args.auto_update_gitignore {
if args.gitignore {
operations::update_project_gitignore(current_dir, &registry, args.nested_depth)?;
print_success("Updated .gitignore with generated file patterns");
} else {
operations::remove_gitignore_section(current_dir, &registry)?;
}
}

Ok(())
Expand Down Expand Up @@ -151,6 +154,7 @@ mod tests {
agents: None,
gitignore: true,
nested_depth: NESTED_DEPTH,
auto_update_gitignore: true,
};

const TEST_RULE_CONTENT: &str = r#"---
Expand Down Expand Up @@ -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());
Expand All @@ -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());
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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());
Expand All @@ -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());
Expand All @@ -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
"#,
);
}
}
2 changes: 2 additions & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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());
Expand Down
Loading