From 2972cf52c588c61ad442879d9922c5ecdab92707 Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Thu, 26 Mar 2026 15:01:09 +1100 Subject: [PATCH 1/5] remove use_claude_skill configuration as ai rules can handle skills generally --- docs/agents.md | 2 +- docs/commands-and-skills.md | 31 -- docs/configuration.md | 12 - src/agents/claude.rs | 166 +---------- src/agents/registry.rs | 10 +- src/cli/mod.rs | 13 +- src/cli/tests.rs | 10 - src/commands/clean.rs | 26 +- src/commands/generate.rs | 87 ++---- src/commands/list_agents.rs | 4 +- src/commands/mod.rs | 12 +- src/commands/status.rs | 72 ++--- src/config.rs | 49 ---- src/operations/claude_skills.rs | 422 ---------------------------- src/operations/gitignore_updater.rs | 6 +- src/operations/mod.rs | 5 +- src/operations/skills_reader.rs | 39 ++- 17 files changed, 127 insertions(+), 839 deletions(-) delete mode 100644 src/operations/claude_skills.rs diff --git a/docs/agents.md b/docs/agents.md index 92ce885..7b52e10 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -5,7 +5,7 @@ | Agent | Standard Mode | Symlink Mode | MCP Support | Notes | |-------|---------------|--------------|-------------|-------| | **AMP** | `AGENTS.md` -> inlined file | `AGENTS.md` -> `ai-rules/AGENTS.md` | - | | -| **Claude Code** | `CLAUDE.md` -> inlined file (+ `.claude/skills/` if configured) | `CLAUDE.md` -> `ai-rules/AGENTS.md` | `.mcp.json` | Skills support via `use_claude_skills` config | +| **Claude Code** | `CLAUDE.md` -> inlined file | `CLAUDE.md` -> `ai-rules/AGENTS.md` | `.mcp.json` | | | **Cline** | `.clinerules/AGENTS.md` -> inlined file | `.clinerules/AGENTS.md` -> `../ai-rules/AGENTS.md` | - | | | **Codex** | `AGENTS.md` -> inlined file | `AGENTS.md` -> `ai-rules/AGENTS.md` | - | | | **Copilot** | `AGENTS.md` -> inlined file | `AGENTS.md` -> `ai-rules/AGENTS.md` | - | | diff --git a/docs/commands-and-skills.md b/docs/commands-and-skills.md index 89683e8..c437e33 100644 --- a/docs/commands-and-skills.md +++ b/docs/commands-and-skills.md @@ -41,37 +41,6 @@ Command files support optional YAML frontmatter: --- -## Claude Code Skills - -Claude Code supports optional rules through [skills](https://docs.claude.com/en/docs/claude-code/skills). This requires `use_claude_skills: true` in your config. - -When enabled and a source rule has `alwaysApply: false`, the tool generates: -- **CLAUDE.md** - References required rules (`alwaysApply: true`) only -- **.claude/skills/{rule-name}/SKILL.md** - Individual skill files for optional rules - -### Example - -Source file `ai-rules/testing.md`: - -```markdown ---- -description: React Testing Library best practices -alwaysApply: false ---- -# Testing Rules -- Prefer user-centric queries (getByRole, getByLabelText) -- Avoid implementation details (testId, class names) -- Test behavior, not implementation -``` - -Generates: -- `CLAUDE.md` - Contains only required rules -- `.claude/skills/testing/SKILL.md` - Skill that Claude can invoke when working with test files - -Skills use the `description` field as the skill name for better discoverability. - ---- - ## User-Defined Skills You can define custom skill folders that are symlinked to supported agents' skill directories during generation. diff --git a/docs/configuration.md b/docs/configuration.md index 2448423..b034dbd 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -20,15 +20,3 @@ 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 - -## Experimental Options - -### Claude Code Skills Mode - -```yaml -use_claude_skills: true # Default: false -``` - -Experimental toggle to test Claude Code's skills feature. When enabled, rules with `alwaysApply: false` are generated as separate skills in `.claude/skills/` instead of being included in `CLAUDE.md`. This allows Claude Code to selectively apply optional rules based on context. - -See [Commands and Skills](commands-and-skills.md) for more details on skills. diff --git a/src/agents/claude.rs b/src/agents/claude.rs index 02439d6..d965922 100644 --- a/src/agents/claude.rs +++ b/src/agents/claude.rs @@ -6,10 +6,9 @@ use crate::agents::rule_generator::AgentRuleGenerator; use crate::agents::skills_generator::SkillsGeneratorTrait; use crate::constants::{ CLAUDE_COMMANDS_DIR, CLAUDE_COMMANDS_SUBDIR, CLAUDE_MCP_JSON, CLAUDE_SKILLS_DIR, - GENERATED_FILE_PREFIX, }; use crate::models::source_file::SourceFile; -use crate::operations::{claude_skills, generate_inlined_required_content}; +use crate::operations::remove_generated_skill_symlinks; use crate::utils::file_utils::{ check_agents_md_symlink, check_inlined_file_symlink, create_symlink_to_agents_md, create_symlink_to_inlined_file, @@ -22,15 +21,13 @@ use std::path::{Path, PathBuf}; pub struct ClaudeGenerator { name: String, output_filename: String, - skills_mode: bool, } impl ClaudeGenerator { - pub fn new(name: &str, output_filename: &str, skills_mode: bool) -> Self { + pub fn new(name: &str, output_filename: &str) -> Self { Self { name: name.to_string(), output_filename: output_filename.to_string(), - skills_mode, } } } @@ -45,32 +42,17 @@ impl AgentRuleGenerator for ClaudeGenerator { if output_file.exists() || output_file.is_symlink() { fs::remove_file(&output_file)?; } - claude_skills::remove_generated_skills(current_dir)?; + remove_generated_skill_symlinks(current_dir, CLAUDE_SKILLS_DIR)?; Ok(()) } fn generate_agent_contents( &self, - source_files: &[SourceFile], - current_dir: &Path, + _source_files: &[SourceFile], + _current_dir: &Path, ) -> HashMap { - let mut all_files = HashMap::new(); - - if !source_files.is_empty() && self.skills_mode { - // In skills mode: inline required content (not @ refs), skills handle optional - let content = generate_inlined_required_content(source_files); - all_files.insert(current_dir.join(&self.output_filename), content); - - if let Ok(skill_files) = - claude_skills::generate_skills_for_optional_rules(source_files, current_dir) - { - all_files.extend(skill_files); - } - // Non-skills mode is handled by generate_inlined_symlink - } - - all_files + HashMap::new() } fn check_agent_contents( @@ -78,29 +60,13 @@ impl AgentRuleGenerator for ClaudeGenerator { source_files: &[SourceFile], current_dir: &Path, ) -> Result { - let file_path = current_dir.join(&self.output_filename); - if source_files.is_empty() { + let file_path = current_dir.join(&self.output_filename); if file_path.exists() { return Ok(false); } - } else { - if !file_path.exists() { - return Ok(false); - } - // In skills mode: check inlined required content - let expected_content = generate_inlined_required_content(source_files); - let actual_content = fs::read_to_string(&file_path)?; - if actual_content != expected_content { - return Ok(false); - } - } - - if self.skills_mode { - claude_skills::check_skills_in_sync(source_files, current_dir) - } else { - Ok(true) } + Ok(true) } fn check_symlink(&self, current_dir: &Path) -> Result { @@ -109,11 +75,7 @@ impl AgentRuleGenerator for ClaudeGenerator { } fn gitignore_patterns(&self) -> Vec { - let mut patterns = vec![self.output_filename.clone()]; - if self.skills_mode { - patterns.push(format!("{}/{}*", CLAUDE_SKILLS_DIR, GENERATED_FILE_PREFIX)); - } - patterns + vec![self.output_filename.clone()] } fn generate_symlink(&self, current_dir: &Path) -> Result> { @@ -126,7 +88,7 @@ impl AgentRuleGenerator for ClaudeGenerator { } fn uses_inlined_symlink(&self) -> bool { - !self.skills_mode + true } fn generate_inlined_symlink(&self, current_dir: &Path) -> Result> { @@ -158,8 +120,6 @@ impl AgentRuleGenerator for ClaudeGenerator { } fn skills_generator(&self) -> Option> { - // Return a skills generator for user-defined skills in ai-rules/skills/ - // This is separate from the existing optional-rules-based skills Some(Box::new(ExternalSkillsGenerator::new(CLAUDE_SKILLS_DIR))) } } @@ -173,7 +133,7 @@ mod tests { #[test] fn test_clean_removes_both_file_and_skills() { let temp_dir = TempDir::new().unwrap(); - let generator = ClaudeGenerator::new("claude", "CLAUDE.md", true); + let generator = ClaudeGenerator::new("claude", "CLAUDE.md"); create_file(temp_dir.path(), "CLAUDE.md", "content"); @@ -195,18 +155,8 @@ mod tests { } #[test] - fn test_gitignore_patterns_includes_skills() { - let generator = ClaudeGenerator::new("claude", "CLAUDE.md", true); - let patterns = generator.gitignore_patterns(); - - assert_eq!(patterns.len(), 2); - assert!(patterns.contains(&"CLAUDE.md".to_string())); - assert!(patterns.contains(&".claude/skills/ai-rules-generated-*".to_string())); - } - - #[test] - fn test_gitignore_patterns_no_skills_mode() { - let generator = ClaudeGenerator::new("claude", "CLAUDE.md", false); + fn test_gitignore_patterns() { + let generator = ClaudeGenerator::new("claude", "CLAUDE.md"); let patterns = generator.gitignore_patterns(); assert_eq!(patterns.len(), 1); @@ -214,9 +164,9 @@ mod tests { } #[test] - fn test_generate_agent_contents_creates_both() { + fn test_generate_agent_contents_returns_empty() { let temp_dir = TempDir::new().unwrap(); - let generator = ClaudeGenerator::new("claude", "CLAUDE.md", true); + let generator = ClaudeGenerator::new("claude", "CLAUDE.md"); let source_files = vec![ create_test_source_file( "always1", @@ -236,92 +186,6 @@ mod tests { let files = generator.generate_agent_contents(&source_files, temp_dir.path()); - assert_eq!(files.len(), 2); - - let claude_md_path = temp_dir.path().join("CLAUDE.md"); - let claude_content = files.get(&claude_md_path).expect("CLAUDE.md should exist"); - // In skills mode, CLAUDE.md should contain inlined required content with description header - assert_eq!(claude_content, "# Always\n\nAlways content\n"); - - let skill_path = temp_dir - .path() - .join(".claude/skills/ai-rules-generated-optional1/SKILL.md"); - let skill_content = files.get(&skill_path).expect("Skill file should exist"); - assert!(skill_content.contains("name: optional")); - assert!(skill_content.contains("description: Optional")); - assert!( - skill_content.contains("@ai-rules/.generated-ai-rules/ai-rules-generated-optional1.md") - ); - } - - #[test] - fn test_generate_agent_contents_non_skills_mode() { - let temp_dir = TempDir::new().unwrap(); - let generator = ClaudeGenerator::new("claude", "CLAUDE.md", false); - let source_files = vec![ - create_test_source_file( - "always1", - "Always", - true, - vec!["**/*.ts".to_string()], - "Always content", - ), - create_test_source_file( - "optional1", - "Optional", - false, - vec!["**/*.js".to_string()], - "Optional content", - ), - ]; - - let files = generator.generate_agent_contents(&source_files, temp_dir.path()); - - // In non-skills mode, generate_agent_contents returns empty - // (symlinks handle output via generate_inlined_symlink) assert_eq!(files.len(), 0); } - - #[test] - fn test_check_agent_contents_validates_both() { - let temp_dir = TempDir::new().unwrap(); - let generator = ClaudeGenerator::new("claude", "CLAUDE.md", true); - let source_files = vec![ - create_test_source_file("always1", "Always", true, vec![], "Always content"), - create_test_source_file("optional1", "Optional", false, vec![], "Optional content"), - ]; - - // Initially not in sync (no files) - let result = generator - .check_agent_contents(&source_files, temp_dir.path()) - .unwrap(); - assert!(!result); - - // Create CLAUDE.md with inlined required content - let claude_content = generate_inlined_required_content(&source_files); - create_file(temp_dir.path(), "CLAUDE.md", &claude_content); - - // Still not in sync (missing skill) - let result = generator - .check_agent_contents(&source_files, temp_dir.path()) - .unwrap(); - assert!(!result); - - // Create skill file - let skill_dir = temp_dir - .path() - .join(".claude/skills/ai-rules-generated-optional1"); - std::fs::create_dir_all(&skill_dir).unwrap(); - std::fs::write( - skill_dir.join("SKILL.md"), - "---\nname: optional\ndescription: Optional\n---\n\n@ai-rules/.generated-ai-rules/ai-rules-generated-optional1.md", - ) - .unwrap(); - - // Now in sync - let result = generator - .check_agent_contents(&source_files, temp_dir.path()) - .unwrap(); - assert!(result); - } } diff --git a/src/agents/registry.rs b/src/agents/registry.rs index 12d5565..dd926b8 100644 --- a/src/agents/registry.rs +++ b/src/agents/registry.rs @@ -12,15 +12,11 @@ pub struct AgentToolRegistry { } impl AgentToolRegistry { - pub fn new(use_claude_skills: bool) -> Self { + pub fn new() -> Self { let mut tools: HashMap> = HashMap::new(); - // Claude now always uses ClaudeGenerator with skills_mode parameter - let claude_generator: Box = Box::new(ClaudeGenerator::new( - "claude", - "CLAUDE.md", - use_claude_skills, - )); + let claude_generator: Box = + Box::new(ClaudeGenerator::new("claude", "CLAUDE.md")); let generators: Vec> = vec![ claude_generator, diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 5a708f9..d1eafad 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -24,26 +24,21 @@ pub fn run_cli() -> anyhow::Result<()> { let config = config::load_config(¤t_dir)?; - let use_claude_skills = config - .as_ref() - .and_then(|c| c.use_claude_skills) - .unwrap_or(false); - match cli.command { Some(Commands::Init(init_args)) => run_init(¤t_dir, init_args), Some(Commands::Generate(args)) => { let final_args = args.with_config(config.as_ref()); - run_generate(¤t_dir, final_args, use_claude_skills) + run_generate(¤t_dir, final_args) } Some(Commands::Status(args)) => { let final_args = args.with_config(config.as_ref()); - run_status(¤t_dir, final_args, use_claude_skills) + run_status(¤t_dir, final_args) } Some(Commands::Clean(args)) => { let nested_depth = args.nested_depth_args.with_config(config.as_ref()); - run_clean(¤t_dir, nested_depth, use_claude_skills) + run_clean(¤t_dir, nested_depth) } - Some(Commands::ListAgents) => run_list_agents(use_claude_skills), + Some(Commands::ListAgents) => run_list_agents(), None => { // If no command is provided and --summary is not used, show help use clap::CommandFactory; diff --git a/src/cli/tests.rs b/src/cli/tests.rs index 8a9615b..c07cce6 100644 --- a/src/cli/tests.rs +++ b/src/cli/tests.rs @@ -11,7 +11,6 @@ fn test_generate_args_with_config_cli_priority() { gitignore: Some(false), no_gitignore: None, nested_depth: Some(5), - use_claude_skills: None, }; let args = GenerateArgs { @@ -36,7 +35,6 @@ fn test_generate_args_with_config_uses_config_when_cli_missing() { gitignore: Some(true), no_gitignore: None, nested_depth: Some(3), - use_claude_skills: None, }; let args = GenerateArgs { @@ -77,7 +75,6 @@ fn test_generate_args_with_config_partial_config() { gitignore: None, no_gitignore: None, nested_depth: None, - use_claude_skills: None, }; let args = GenerateArgs { @@ -102,7 +99,6 @@ fn test_nested_depth_args_with_config() { gitignore: None, no_gitignore: None, nested_depth: Some(4), - use_claude_skills: None, }; let args_with_cli = NestedDepthArgs { @@ -125,7 +121,6 @@ fn test_nested_depth_explicit_zero_overrides_config() { gitignore: None, no_gitignore: None, nested_depth: Some(5), - use_claude_skills: None, }; let args = NestedDepthArgs { @@ -143,7 +138,6 @@ fn test_status_args_with_config_cli_priority() { gitignore: None, no_gitignore: None, nested_depth: Some(5), - use_claude_skills: None, }; let args = StatusArgs { @@ -167,7 +161,6 @@ fn test_status_args_with_config_uses_config_when_cli_missing() { gitignore: None, no_gitignore: None, nested_depth: Some(3), - use_claude_skills: None, }; let args = StatusArgs { @@ -202,7 +195,6 @@ fn test_generate_args_backward_compat_no_gitignore_config() { gitignore: None, no_gitignore: Some(true), nested_depth: None, - use_claude_skills: None, }; let args = GenerateArgs { @@ -225,7 +217,6 @@ fn test_generate_args_backward_compat_no_gitignore_cli() { gitignore: Some(true), no_gitignore: None, nested_depth: None, - use_claude_skills: None, }; let args = GenerateArgs { @@ -248,7 +239,6 @@ fn test_generate_args_new_gitignore_flag_overrides_old() { gitignore: None, no_gitignore: None, nested_depth: None, - use_claude_skills: None, }; let args = GenerateArgs { diff --git a/src/commands/clean.rs b/src/commands/clean.rs index 169d884..a9427c4 100644 --- a/src/commands/clean.rs +++ b/src/commands/clean.rs @@ -4,9 +4,9 @@ use crate::utils::file_utils; use anyhow::Result; use std::path::Path; -pub fn run_clean(current_dir: &Path, nested_depth: usize, use_claude_skills: bool) -> Result<()> { +pub fn run_clean(current_dir: &Path, nested_depth: usize) -> Result<()> { println!("📋 Cleaning files for all agents, nested_depth: {nested_depth}"); - let registry = AgentToolRegistry::new(use_claude_skills); + let registry = AgentToolRegistry::new(); let agents: Vec = registry.get_all_tool_names(); let filter = file_utils::DirectoryFilter::from_project_root(current_dir); @@ -46,7 +46,7 @@ mod tests { create_file(project_path, "ai-rules/test.md", "Original rule"); create_file(project_path, "src/main.ts", "console.log('test');"); - let result = run_clean(project_path, CLEAN_NESTED_DEPTH, false); + let result = run_clean(project_path, CLEAN_NESTED_DEPTH); assert!(result.is_ok()); assert_file_not_exists(project_path, "CLAUDE.md"); @@ -87,7 +87,7 @@ mod tests { ); create_file(project_path, "nested/deep/subproject2/src/code.ts", "code"); - let result = run_clean(project_path, CLEAN_NESTED_DEPTH, false); + let result = run_clean(project_path, CLEAN_NESTED_DEPTH); assert!(result.is_ok()); assert_file_not_exists(project_path, "subproject1/CLAUDE.md"); @@ -119,7 +119,7 @@ mod tests { create_file(project_path, "src/main.rs", "fn main() {}"); - let result = run_clean(project_path, CLEAN_NESTED_DEPTH, false); + let result = run_clean(project_path, CLEAN_NESTED_DEPTH); assert!(result.is_ok()); assert_file_not_exists(project_path, "CLAUDE.md"); @@ -163,7 +163,6 @@ Test rule content"#; gitignore: false, nested_depth: 2, }, - false, ); assert!(generate_result.is_ok()); @@ -171,7 +170,7 @@ Test rule content"#; assert_file_exists(project_path, "level1/CLAUDE.md"); assert_file_exists(project_path, "level1/level2/CLAUDE.md"); - let clean_result = run_clean(project_path, 0, false); + let clean_result = run_clean(project_path, 0); assert!(clean_result.is_ok()); assert_file_not_exists(project_path, "CLAUDE.md"); @@ -213,7 +212,6 @@ Test rule content"#; gitignore: false, nested_depth: CLEAN_NESTED_DEPTH, }, - false, ); assert!(generate_result.is_ok()); @@ -235,7 +233,7 @@ Test rule content"#; assert_file_exists(project_path, file); } - let clean_result = run_clean(project_path, CLEAN_NESTED_DEPTH, false); + let clean_result = run_clean(project_path, CLEAN_NESTED_DEPTH); assert!(clean_result.is_ok()); for file in &expected_files { @@ -278,7 +276,7 @@ Test rule content"#; "old kilocode content", ); - let clean_result = run_clean(project_path, CLEAN_NESTED_DEPTH, false); + let clean_result = run_clean(project_path, CLEAN_NESTED_DEPTH); assert!(clean_result.is_ok()); // Legacy directories should be cleaned up @@ -301,7 +299,7 @@ Test rule content"#; create_file(project_path, ".roo/rules/my-custom-rule.md", "user file"); create_file(project_path, ".roo/custom-config.txt", "user config"); - let clean_result = run_clean(project_path, CLEAN_NESTED_DEPTH, false); + let clean_result = run_clean(project_path, CLEAN_NESTED_DEPTH); assert!(clean_result.is_ok()); // Generated file should be removed by legacy cleaner @@ -335,7 +333,6 @@ Test rule content"#; gitignore: false, nested_depth: CLEAN_NESTED_DEPTH, }, - false, ); assert!(generate_result.is_ok()); @@ -348,7 +345,7 @@ Test rule content"#; assert!(symlink_path.is_symlink()); // Clean - let clean_result = run_clean(project_path, CLEAN_NESTED_DEPTH, false); + let clean_result = run_clean(project_path, CLEAN_NESTED_DEPTH); assert!(clean_result.is_ok()); // Verify skill symlink was removed @@ -382,7 +379,6 @@ Test rule content"#; gitignore: false, nested_depth: CLEAN_NESTED_DEPTH, }, - false, ); assert!(generate_result.is_ok()); @@ -392,7 +388,7 @@ Test rule content"#; std::fs::write(user_skill_dir.join("SKILL.md"), "user skill content").unwrap(); // Clean - let clean_result = run_clean(project_path, CLEAN_NESTED_DEPTH, false); + let clean_result = run_clean(project_path, CLEAN_NESTED_DEPTH); assert!(clean_result.is_ok()); // Generated symlink should be removed diff --git a/src/commands/generate.rs b/src/commands/generate.rs index 073e3a2..87d7841 100644 --- a/src/commands/generate.rs +++ b/src/commands/generate.rs @@ -10,11 +10,7 @@ use anyhow::Result; use std::collections::HashMap; use std::path::{Path, PathBuf}; -pub fn run_generate( - current_dir: &Path, - args: ResolvedGenerateArgs, - use_claude_skills: bool, -) -> Result<()> { +pub fn run_generate(current_dir: &Path, args: ResolvedGenerateArgs) -> Result<()> { println!( "Generating rules for agents: {}, nested_depth: {}, gitignore: {}", args.agents @@ -24,7 +20,7 @@ pub fn run_generate( args.nested_depth, args.gitignore ); - let registry = AgentToolRegistry::new(use_claude_skills); + let registry = AgentToolRegistry::new(); let agents = args.agents.unwrap_or_else(|| registry.get_all_tool_names()); let command_agents = args.command_agents.unwrap_or_else(|| agents.clone()); @@ -172,7 +168,7 @@ Test rule content"#; fn test_run_generate_empty_project() { let temp_dir = TempDir::new().unwrap(); - let result = run_generate(temp_dir.path(), GENERATE_ARGS, false); + let result = run_generate(temp_dir.path(), GENERATE_ARGS); assert!(result.is_ok()); assert_file_exists(temp_dir.path(), ".gitignore"); @@ -188,7 +184,7 @@ Test rule content"#; create_file(temp_dir.path(), "ai-rules/test.md", TEST_RULE_CONTENT); - let result = run_generate(temp_dir.path(), GENERATE_ARGS, false); + let result = run_generate(temp_dir.path(), GENERATE_ARGS); assert!(result.is_ok()); assert_file_exists( @@ -245,7 +241,7 @@ Test rule content"#; gitignore: false, nested_depth: NESTED_DEPTH, }; - let result = run_generate(temp_dir.path(), args, false); + let result = run_generate(temp_dir.path(), args); assert!(result.is_ok()); assert_file_exists( @@ -268,7 +264,7 @@ Test rule content"#; gitignore: true, nested_depth: NESTED_DEPTH, }; - let result = run_generate(temp_dir.path(), args, false); + let result = run_generate(temp_dir.path(), args); assert!(result.is_ok()); assert_file_exists( @@ -308,7 +304,7 @@ Test rule content"#; TEST_RULE_CONTENT, ); - let result = run_generate(temp_dir.path(), GENERATE_ARGS, false); + let result = run_generate(temp_dir.path(), GENERATE_ARGS); assert!(result.is_ok()); assert_file_exists( @@ -342,7 +338,7 @@ Test rule content"#; create_file(temp_dir.path(), "ai-rules/test.md", TEST_RULE_CONTENT); - let result = run_generate(temp_dir.path(), GENERATE_ARGS, false); + let result = run_generate(temp_dir.path(), GENERATE_ARGS); assert!(result.is_ok()); // Check that gitignore contains patterns with ** prefix for subdirectory matching @@ -371,7 +367,7 @@ Test rule content"#; gitignore: true, nested_depth: 0, }; - let result = run_generate(temp_dir.path(), args, false); + let result = run_generate(temp_dir.path(), args); assert!(result.is_ok()); assert_file_exists( @@ -413,7 +409,7 @@ Test rule content"#; gitignore: false, nested_depth: NESTED_DEPTH, }; - let result = run_generate(temp_dir.path(), args, false); + let result = run_generate(temp_dir.path(), args); assert!(result.is_ok()); assert_file_exists(temp_dir.path(), "CLAUDE.md"); @@ -437,7 +433,7 @@ Test rule content"#; #[test] fn test_generate_files_symlink_mode() { let temp_dir = TempDir::new().unwrap(); - let registry = AgentToolRegistry::new(false); + let registry = AgentToolRegistry::new(); create_file( temp_dir.path(), @@ -481,7 +477,7 @@ Test rule content"#; #[test] fn test_generate_files_symlink_mode_cleans_normal_files() { let temp_dir = TempDir::new().unwrap(); - let registry = AgentToolRegistry::new(false); + let registry = AgentToolRegistry::new(); // First create normal files create_file(temp_dir.path(), "CLAUDE.md", "@.generated-ai-rules/old.md"); @@ -516,7 +512,7 @@ Test rule content"#; #[test] fn test_generation_result_agent_listing_symlink_mode() { let temp_dir = TempDir::new().unwrap(); - let registry = AgentToolRegistry::new(false); + let registry = AgentToolRegistry::new(); create_file(temp_dir.path(), "ai-rules/AGENTS.md", "# Pure content"); let agents = vec!["claude".to_string(), "goose".to_string()]; @@ -547,7 +543,7 @@ Test rule content"#; #[test] fn test_generation_result_agent_listing_normal_mode() { let temp_dir = TempDir::new().unwrap(); - let registry = AgentToolRegistry::new(false); + let registry = AgentToolRegistry::new(); create_file(temp_dir.path(), "ai-rules/test.md", TEST_RULE_CONTENT); let agents = vec!["claude".to_string(), "cursor".to_string()]; @@ -577,7 +573,7 @@ Test rule content"#; #[test] fn test_generate_files_normal_mode_cleans_symlinks() { let temp_dir = TempDir::new().unwrap(); - let registry = AgentToolRegistry::new(false); + let registry = AgentToolRegistry::new(); create_file(temp_dir.path(), "ai-rules/AGENTS.md", "# Pure content"); let agents = vec!["claude".to_string()]; @@ -627,43 +623,6 @@ New body content"#; assert_eq!(claude_content, "# New rule\n\nNew body content\n"); } - #[test] - fn test_generate_claude_skills_mode_vs_single_file_mode() { - let temp_dir = TempDir::new().unwrap(); - - // Create a simple optional rule - create_file( - temp_dir.path(), - "ai-rules/optional.md", - r#"--- -description: Optional rule -alwaysApply: false ---- -Optional content"#, - ); - - let args = ResolvedGenerateArgs { - agents: Some(vec!["claude".to_string()]), - command_agents: None, - gitignore: false, - nested_depth: NESTED_DEPTH, - }; - run_generate(temp_dir.path(), args.clone(), true).unwrap(); - - assert_file_exists( - temp_dir.path(), - ".claude/skills/ai-rules-generated-optional/SKILL.md", - ); - - std::fs::remove_dir_all(temp_dir.path().join(".claude")).unwrap(); - std::fs::remove_file(temp_dir.path().join("CLAUDE.md")).unwrap(); - - run_generate(temp_dir.path(), args, false).unwrap(); - - assert_file_not_exists(temp_dir.path(), ".claude/skills/"); - assert_file_exists(temp_dir.path(), "ai-rules/.generated-ai-rules"); - } - const TEST_MCP_CONFIG: &str = r#"{ "mcpServers": { "test-server": { @@ -690,7 +649,7 @@ Optional content"#, gitignore: false, nested_depth: NESTED_DEPTH, }; - let result = run_generate(temp_dir.path(), args, false); + let result = run_generate(temp_dir.path(), args); assert!(result.is_ok()); assert_file_exists(temp_dir.path(), "CLAUDE.md"); @@ -726,7 +685,7 @@ Optional content"#, gitignore: false, nested_depth: NESTED_DEPTH, }; - let result = run_generate(temp_dir.path(), args, false); + let result = run_generate(temp_dir.path(), args); assert!(result.is_ok()); // Agent files should be created @@ -751,7 +710,7 @@ Optional content"#, gitignore: false, nested_depth: NESTED_DEPTH, }; - let result = run_generate(temp_dir.path(), args, false); + let result = run_generate(temp_dir.path(), args); assert!(result.is_ok()); assert_file_exists(temp_dir.path(), "firebender.json"); @@ -778,7 +737,7 @@ Optional content"#, gitignore: false, nested_depth: NESTED_DEPTH, }; - let result = run_generate(temp_dir.path(), args, false); + let result = run_generate(temp_dir.path(), args); assert!(result.is_ok()); // Rule files: only AMP (AGENTS.md), no CLAUDE.md @@ -817,7 +776,7 @@ Optional content"#, gitignore: false, nested_depth: NESTED_DEPTH, }; - let result = run_generate(temp_dir.path(), args, false); + let result = run_generate(temp_dir.path(), args); assert!(result.is_ok()); // Both rules and commands for claude only @@ -853,7 +812,7 @@ Optional content"#, gitignore: false, nested_depth: NESTED_DEPTH, }; - let result = run_generate(temp_dir.path(), args, false); + let result = run_generate(temp_dir.path(), args); assert!(result.is_ok()); // Verify skill symlink was created @@ -887,7 +846,7 @@ Optional content"#, gitignore: false, nested_depth: NESTED_DEPTH, }; - let result = run_generate(temp_dir.path(), args, false); + let result = run_generate(temp_dir.path(), args); assert!(result.is_ok()); // Verify skill symlink was created in .agents/skills/ @@ -911,7 +870,7 @@ Optional content"#, gitignore: false, nested_depth: NESTED_DEPTH, }; - let result = run_generate(temp_dir.path(), args, false); + let result = run_generate(temp_dir.path(), args); assert!(result.is_ok()); // Verify no skill symlinks created (skills directory shouldn't exist) diff --git a/src/commands/list_agents.rs b/src/commands/list_agents.rs index 2e8a87a..f6774ca 100644 --- a/src/commands/list_agents.rs +++ b/src/commands/list_agents.rs @@ -1,7 +1,7 @@ use crate::agents::AgentToolRegistry; -pub fn run_list_agents(use_claude_skills: bool) -> anyhow::Result<()> { - let registry = AgentToolRegistry::new(use_claude_skills); +pub fn run_list_agents() -> anyhow::Result<()> { + let registry = AgentToolRegistry::new(); let mut agent_names = registry.get_all_tool_names(); agent_names.sort(); diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 9fc7818..641ccd0 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -72,7 +72,7 @@ mod tests { gitignore: true, nested_depth, }; - let generate_result = run_generate(project_path, generate_args, false); + let generate_result = run_generate(project_path, generate_args); if let Err(e) = &generate_result { panic!("Generate failed with error: {e}"); } @@ -91,7 +91,7 @@ mod tests { command_agents: None, nested_depth, }; - let status_result = check_project_status(project_path, status_args, false).unwrap(); + let status_result = check_project_status(project_path, status_args).unwrap(); assert!(status_result.has_ai_rules); assert!(!status_result.body_files_out_of_sync); for in_sync in status_result.agent_statuses.values() { @@ -108,7 +108,7 @@ mod tests { command_agents: None, nested_depth, }; - let status_after_change = check_project_status(project_path, status_args, false).unwrap(); + let status_after_change = check_project_status(project_path, status_args).unwrap(); assert!(status_after_change.has_ai_rules); assert!(!status_after_change.body_files_out_of_sync); @@ -126,7 +126,7 @@ mod tests { ); // Clean - should remove all generated files - let clean_result = run_clean(project_path, nested_depth, false); + let clean_result = run_clean(project_path, nested_depth); assert!(clean_result.is_ok()); assert_file_not_exists(project_path, "ai-rules/.generated-ai-rules"); @@ -153,7 +153,7 @@ mod tests { gitignore: true, nested_depth, }; - let generate_result = run_generate(project_path, generate_args, false); + let generate_result = run_generate(project_path, generate_args); assert!(generate_result.is_ok()); // Verify all agents created symlinks pointing to the correct target @@ -178,7 +178,7 @@ mod tests { command_agents: None, nested_depth, }; - let status_after_change = check_project_status(project_path, status_args, false).unwrap(); + let status_after_change = check_project_status(project_path, status_args).unwrap(); assert!(status_after_change.has_ai_rules); assert!(!status_after_change.body_files_out_of_sync); diff --git a/src/commands/status.rs b/src/commands/status.rs index a3f17c4..ddbd2e3 100644 --- a/src/commands/status.rs +++ b/src/commands/status.rs @@ -27,11 +27,7 @@ impl std::fmt::Display for BodyFilesOutOfSync { impl std::error::Error for BodyFilesOutOfSync {} -pub fn run_status( - current_dir: &Path, - args: ResolvedStatusArgs, - use_claude_skills: bool, -) -> Result<()> { +pub fn run_status(current_dir: &Path, args: ResolvedStatusArgs) -> Result<()> { println!( "🔍 AI Rules Status for agents: {}, nested_depth: {}", args.agents @@ -41,18 +37,14 @@ pub fn run_status( args.nested_depth ); - let status = check_project_status(current_dir, args, use_claude_skills)?; + let status = check_project_status(current_dir, args)?; print_status_results(&status); Ok(()) } -pub fn check_project_status( - current_dir: &Path, - args: ResolvedStatusArgs, - use_claude_skills: bool, -) -> Result { - let registry = AgentToolRegistry::new(use_claude_skills); +pub fn check_project_status(current_dir: &Path, args: ResolvedStatusArgs) -> Result { + let registry = AgentToolRegistry::new(); let agents: Vec = args.agents.unwrap_or_else(|| registry.get_all_tool_names()); // Determine command agents - use command_agents if specified, otherwise fall back to agents @@ -287,7 +279,7 @@ Test rule content"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false); + let result = check_project_status(temp_dir.path(), args); assert!(result.is_ok()); let status = result.unwrap(); @@ -307,7 +299,7 @@ Test rule content"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false); + let result = check_project_status(temp_dir.path(), args); assert!(result.is_ok()); let status = result.unwrap(); @@ -328,7 +320,7 @@ Test rule content"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false); + let result = check_project_status(temp_dir.path(), args); assert!(result.is_ok()); let status = result.unwrap(); @@ -355,7 +347,7 @@ Test rule content"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false); + let result = check_project_status(temp_dir.path(), args); assert!(result.is_ok()); let status = result.unwrap(); @@ -377,7 +369,6 @@ Test rule content"#; gitignore: false, nested_depth: NESTED_DEPTH, }, - false, ) .unwrap(); @@ -394,7 +385,7 @@ Test rule content"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false); + let result = check_project_status(temp_dir.path(), args); assert!(result.is_ok()); let status = result.unwrap(); @@ -430,7 +421,7 @@ Test rule content"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false); + let result = check_project_status(temp_dir.path(), args); assert!(result.is_ok()); let status = result.unwrap(); @@ -462,7 +453,6 @@ Test rule content"#; gitignore: false, nested_depth, }, - false, ) .unwrap(); @@ -471,7 +461,7 @@ Test rule content"#; command_agents: None, nested_depth, }; - let result = check_project_status(temp_dir.path(), args, false); + let result = check_project_status(temp_dir.path(), args); assert!(result.is_ok()); let status = result.unwrap(); @@ -484,7 +474,7 @@ Test rule content"#; command_agents: None, nested_depth: 1, }; - let result = check_project_status(temp_dir.path(), args, false); + let result = check_project_status(temp_dir.path(), args); assert!(result.is_ok()); let status = result.unwrap(); @@ -508,7 +498,6 @@ Test rule content"#; gitignore: false, nested_depth: 1, }, - false, ) .unwrap(); @@ -517,7 +506,7 @@ Test rule content"#; command_agents: None, nested_depth: 1, }; - let result = check_project_status(temp_dir.path(), args, false); + let result = check_project_status(temp_dir.path(), args); assert!(result.is_ok()); let status = result.unwrap(); @@ -543,7 +532,6 @@ Test rule content"#; gitignore: false, nested_depth: NESTED_DEPTH, }, - false, ) .unwrap(); @@ -552,7 +540,7 @@ Test rule content"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false); + let result = check_project_status(temp_dir.path(), args); assert!(result.is_ok()); let status = result.unwrap(); @@ -587,7 +575,6 @@ Test rule content"#; gitignore: false, nested_depth: NESTED_DEPTH, }, - false, ) .unwrap(); } @@ -609,7 +596,7 @@ Test rule content"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false); + let result = check_project_status(temp_dir.path(), args); assert!(result.is_ok()); let status = result.unwrap(); @@ -636,7 +623,7 @@ Test rule content"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false); + let result = check_project_status(temp_dir.path(), args); assert!(result.is_ok()); let status = result.unwrap(); @@ -661,7 +648,6 @@ Test rule content"#; gitignore: false, nested_depth: NESTED_DEPTH, }, - false, ); assert!(generate_result.is_ok()); @@ -670,7 +656,7 @@ Test rule content"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false); + let result = check_project_status(temp_dir.path(), args); assert!(result.is_ok()); let status = result.unwrap(); @@ -703,7 +689,6 @@ Test command body"#; gitignore: false, nested_depth: NESTED_DEPTH, }, - false, ) .unwrap(); } @@ -731,7 +716,7 @@ Test command body"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false); + let result = check_project_status(temp_dir.path(), args); assert!(result.is_ok()); let status = result.unwrap(); @@ -763,7 +748,6 @@ Test command body"#; gitignore: false, nested_depth: NESTED_DEPTH, }, - false, ) .unwrap(); @@ -778,7 +762,7 @@ Test command body"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false); + let result = check_project_status(temp_dir.path(), args); assert!(result.is_ok()); let status = result.unwrap(); @@ -810,7 +794,6 @@ Test command body"#; gitignore: false, nested_depth: NESTED_DEPTH, }, - false, ); assert!(generate_result.is_ok()); @@ -828,7 +811,7 @@ Test command body"#; command_agents: Some(vec!["claude".to_string(), "amp".to_string()]), nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false); + let result = check_project_status(temp_dir.path(), args); assert!(result.is_ok()); let status = result.unwrap(); @@ -864,7 +847,6 @@ Test command body"#; gitignore: false, nested_depth: NESTED_DEPTH, }, - false, ); assert!(generate_result.is_ok()); @@ -873,7 +855,7 @@ Test command body"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false); + let result = check_project_status(temp_dir.path(), args); assert!(result.is_ok()); let status = result.unwrap(); @@ -905,7 +887,6 @@ Test command body"#; gitignore: false, nested_depth: NESTED_DEPTH, }, - false, ) .unwrap(); @@ -924,7 +905,7 @@ Test command body"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false); + let result = check_project_status(temp_dir.path(), args); assert!(result.is_ok()); let status = result.unwrap(); @@ -954,7 +935,6 @@ Test command body"#; gitignore: false, nested_depth: NESTED_DEPTH, }, - false, ); assert!(generate_result.is_ok()); @@ -963,7 +943,7 @@ Test command body"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false); + let result = check_project_status(temp_dir.path(), args); assert!(result.is_ok()); let status = result.unwrap(); @@ -989,7 +969,6 @@ Test command body"#; gitignore: false, nested_depth: NESTED_DEPTH, }, - false, ); assert!(generate_result.is_ok()); @@ -998,7 +977,7 @@ Test command body"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false); + let result = check_project_status(temp_dir.path(), args); assert!(result.is_ok()); let status = result.unwrap(); @@ -1029,7 +1008,6 @@ Test command body"#; gitignore: false, nested_depth: NESTED_DEPTH, }, - false, ); assert!(generate_result.is_ok()); @@ -1041,7 +1019,7 @@ Test command body"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false); + let result = check_project_status(temp_dir.path(), args); assert!(result.is_ok()); let status = result.unwrap(); diff --git a/src/config.rs b/src/config.rs index bc02a1c..3d9685a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,7 +11,6 @@ pub struct Config { pub gitignore: Option, pub no_gitignore: Option, pub nested_depth: Option, - pub use_claude_skills: Option, } pub fn load_config(current_dir: &Path) -> Result> { @@ -126,54 +125,6 @@ nested_depth: 1 assert!(result.is_err()); } - #[test] - fn test_load_config_with_use_claude_skills() { - let temp_dir = TempDir::new().unwrap(); - let config_content = r#" -agents: ["claude"] -use_claude_skills: 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.agents, Some(vec!["claude".to_string()])); - assert_eq!(config.use_claude_skills, Some(true)); - } - - #[test] - fn test_load_config_use_claude_skills_false() { - let temp_dir = TempDir::new().unwrap(); - let config_content = r#" -agents: ["claude"] -use_claude_skills: 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.use_claude_skills, Some(false)); - } - - #[test] - fn test_load_config_without_use_claude_skills_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.use_claude_skills.is_none()); - } - #[test] fn test_load_config_backward_compatibility_no_gitignore() { let temp_dir = TempDir::new().unwrap(); diff --git a/src/operations/claude_skills.rs b/src/operations/claude_skills.rs deleted file mode 100644 index ac86ddd..0000000 --- a/src/operations/claude_skills.rs +++ /dev/null @@ -1,422 +0,0 @@ -use crate::constants::{CLAUDE_SKILLS_DIR, GENERATED_FILE_PREFIX, SKILL_FILENAME}; -use crate::models::source_file::SourceFile; -use crate::operations::body_generator::generated_body_file_reference_path; -use regex::Regex; -use std::collections::HashMap; -use std::path::{Path, PathBuf}; - -/// Sanitizes a skill name to meet Claude's requirements: -/// - Must use lowercase letters, numbers, and hyphens only -/// - Max 64 characters -/// https://docs.claude.com/en/docs/claude-code/skills#write-skill-md -fn sanitize_skill_name(name: &str) -> String { - // Convert to lowercase and replace non-alphanumeric characters with hyphens - let lowercase = name.to_lowercase(); - let re = Regex::new(r"[^a-z0-9-]+").unwrap(); - let with_hyphens = re.replace_all(&lowercase, "-"); - - // Collapse multiple consecutive hyphens into one - let re_multiple = Regex::new(r"-+").unwrap(); - let collapsed = re_multiple.replace_all(&with_hyphens, "-"); - - // Remove leading/trailing hyphens and truncate to 64 characters - let trimmed = collapsed.trim_matches('-'); - let truncated = if trimmed.len() > 64 { - &trimmed[..64] - } else { - trimmed - }; - - truncated.trim_end_matches('-').to_string() -} - -pub fn remove_generated_skills(project_root: &Path) -> anyhow::Result<()> { - let skills_dir = project_root.join(CLAUDE_SKILLS_DIR); - if !skills_dir.exists() { - return Ok(()); - } - - for entry in std::fs::read_dir(&skills_dir)? { - let entry = entry?; - let path = entry.path(); - - if path.is_dir() { - if let Some(folder_name) = path.file_name().and_then(|n| n.to_str()) { - if folder_name.starts_with(GENERATED_FILE_PREFIX) { - std::fs::remove_dir_all(&path)?; - } - } - } - } - - Ok(()) -} - -pub fn generate_skills_for_optional_rules( - source_files: &[SourceFile], - project_root: &Path, -) -> anyhow::Result> { - let mut skill_files = HashMap::new(); - - let optional_rules: Vec<&SourceFile> = source_files - .iter() - .filter(|f| !f.front_matter.always_apply) - .collect(); - - for rule in optional_rules { - let (path, content) = generate_skill_file_content(rule, project_root)?; - skill_files.insert(path, content); - } - - Ok(skill_files) -} - -fn generate_skill_file_content( - rule: &SourceFile, - project_root: &Path, -) -> anyhow::Result<(PathBuf, String)> { - let skill_folder_name = format!("{}{}", GENERATED_FILE_PREFIX, rule.base_file_name); - let skill_file_path = project_root - .join(CLAUDE_SKILLS_DIR) - .join(&skill_folder_name) - .join(SKILL_FILENAME); - - let description = if rule.front_matter.description.is_empty() { - &rule.base_file_name - } else { - &rule.front_matter.description - }; - - let skill_name = sanitize_skill_name(description); - - let body_file_name = rule.get_body_file_name(); - let generated_path = generated_body_file_reference_path(&body_file_name); - - let skill_content = format!( - "---\nname: {}\ndescription: {}\n---\n\n@{}", - skill_name, - description, - generated_path.display() - ); - - Ok((skill_file_path, skill_content)) -} - -pub fn check_skills_in_sync( - source_files: &[SourceFile], - project_root: &Path, -) -> anyhow::Result { - let expected_skills = generate_skills_for_optional_rules(source_files, project_root)?; - let skills_dir = project_root.join(CLAUDE_SKILLS_DIR); - - let actual_generated_skill_dirs: Vec<_> = if skills_dir.exists() { - std::fs::read_dir(&skills_dir)? - .filter_map(Result::ok) - .filter(|entry| { - let path = entry.path(); - // Only count directories, not symlinks (symlinks are handled by ExternalSkillsGenerator) - path.is_dir() - && !path.is_symlink() - && entry - .file_name() - .to_str() - .is_some_and(|n| n.starts_with(GENERATED_FILE_PREFIX)) - }) - .collect() - } else { - vec![] - }; - if expected_skills.is_empty() { - return Ok(actual_generated_skill_dirs.is_empty()); - } - if actual_generated_skill_dirs.is_empty() { - return Ok(false); - } - if actual_generated_skill_dirs.len() != expected_skills.len() { - return Ok(false); - } - for (expected_path, expected_content) in &expected_skills { - if !expected_path.exists() { - return Ok(false); - } - let actual_content = std::fs::read_to_string(expected_path)?; - if actual_content != *expected_content { - return Ok(false); - } - } - - Ok(true) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::models::source_file::FrontMatter; - use std::path::Path; - use tempfile::TempDir; - - fn create_test_source_file( - base_name: &str, - description: &str, - always_apply: bool, - body: &str, - ) -> SourceFile { - SourceFile { - front_matter: FrontMatter { - description: description.to_string(), - always_apply, - file_matching_patterns: None, - }, - body: body.to_string(), - base_file_name: base_name.to_string(), - } - } - - fn create_skill_file(temp_dir: &Path, skill_name: &str, content: &str) -> PathBuf { - let skill_dir = temp_dir.join(format!(".claude/skills/{skill_name}")); - std::fs::create_dir_all(&skill_dir).unwrap(); - std::fs::write(skill_dir.join("SKILL.md"), content).unwrap(); - skill_dir - } - - fn generate_skill_content(name: &str, description: &str, base_name: &str) -> String { - format!( - "---\nname: {name}\ndescription: {description}\n---\n\n@ai-rules/.generated-ai-rules/ai-rules-generated-{base_name}.md" - ) - } - - #[test] - fn test_sanitize_skill_name() { - assert_eq!(sanitize_skill_name("Test Workflow"), "test-workflow"); - assert_eq!(sanitize_skill_name("My_Cool_Rule"), "my-cool-rule"); - assert_eq!(sanitize_skill_name("Rule with CAPS"), "rule-with-caps"); - assert_eq!(sanitize_skill_name("Rule123"), "rule123"); - assert_eq!( - sanitize_skill_name("Rule Multiple Spaces"), - "rule-multiple-spaces" - ); - assert_eq!( - sanitize_skill_name("Rule-with-hyphens"), - "rule-with-hyphens" - ); - assert_eq!( - sanitize_skill_name("Rule_with_underscores"), - "rule-with-underscores" - ); - assert_eq!(sanitize_skill_name("Rule!@#$%Special"), "rule-special"); - assert_eq!(sanitize_skill_name("---leading-hyphens"), "leading-hyphens"); - assert_eq!( - sanitize_skill_name("trailing-hyphens---"), - "trailing-hyphens" - ); - assert_eq!( - sanitize_skill_name("multiple---hyphens"), - "multiple-hyphens" - ); - - let long_name = "a".repeat(100); - assert_eq!(sanitize_skill_name(&long_name).len(), 64); - } - - #[test] - fn test_generate_skill_file_content() { - let temp_dir = TempDir::new().unwrap(); - let source_file = - create_test_source_file("test-optional", "Test Workflow", false, "My workflow is 42"); - - let (path, content) = generate_skill_file_content(&source_file, temp_dir.path()).unwrap(); - - assert_eq!( - path, - temp_dir - .path() - .join(".claude/skills/ai-rules-generated-test-optional/SKILL.md") - ); - assert!(content.contains("name: test-workflow")); - assert!(content.contains("description: Test Workflow")); - assert!( - content.contains("@ai-rules/.generated-ai-rules/ai-rules-generated-test-optional.md") - ); - } - - #[test] - fn test_generate_skills_filters_optional_only() { - let temp_dir = TempDir::new().unwrap(); - let source_files = vec![ - create_test_source_file("optional", "Optional Rule", false, "Optional content"), - create_test_source_file("required", "Required Rule", true, "Required content"), - ]; - - let skills = generate_skills_for_optional_rules(&source_files, temp_dir.path()).unwrap(); - - assert_eq!(skills.len(), 1); - assert!(skills.contains_key( - &temp_dir - .path() - .join(".claude/skills/ai-rules-generated-optional/SKILL.md") - )); - assert!(!skills.contains_key( - &temp_dir - .path() - .join(".claude/skills/ai-rules-generated-required/SKILL.md") - )); - } - - #[test] - fn test_empty_description_uses_filename() { - let temp_dir = TempDir::new().unwrap(); - let source_file = create_test_source_file("fallback-name", "", false, "Content"); - - let (path, content) = generate_skill_file_content(&source_file, temp_dir.path()).unwrap(); - - assert_eq!( - path, - temp_dir - .path() - .join(".claude/skills/ai-rules-generated-fallback-name/SKILL.md") - ); - assert!(content.contains("name: fallback-name")); - assert!(content.contains("description: fallback-name")); - } - - #[test] - fn test_remove_generated_skills() { - let temp_dir = TempDir::new().unwrap(); - - let generated_skill = create_skill_file( - temp_dir.path(), - "ai-rules-generated-test", - "generated content", - ); - let user_skill = create_skill_file(temp_dir.path(), "my-custom-skill", "user content"); - - remove_generated_skills(temp_dir.path()).unwrap(); - - assert!(!generated_skill.exists()); - assert!(user_skill.exists()); - assert_eq!( - std::fs::read_to_string(user_skill.join("SKILL.md")).unwrap(), - "user content" - ); - } - - #[test] - fn test_check_skills_in_sync_no_optional_rules() { - let temp_dir = TempDir::new().unwrap(); - let source_files = vec![create_test_source_file( - "required", "Required", true, "Content", - )]; - - let result = check_skills_in_sync(&source_files, temp_dir.path()).unwrap(); - assert!(result); - - create_skill_file(temp_dir.path(), "my-custom-skill", "custom"); - - let result = check_skills_in_sync(&source_files, temp_dir.path()).unwrap(); - assert!(result); - } - - #[test] - fn test_check_skills_in_sync_missing_skill_file() { - let temp_dir = TempDir::new().unwrap(); - let source_files = vec![create_test_source_file( - "optional", "Optional", false, "Content", - )]; - - let result = check_skills_in_sync(&source_files, temp_dir.path()).unwrap(); - assert!(!result); - } - - #[test] - fn test_check_skills_in_sync_with_orphaned_skill() { - let temp_dir = TempDir::new().unwrap(); - let source_files = vec![create_test_source_file( - "optional", "Optional", false, "Content", - )]; - - create_skill_file( - temp_dir.path(), - "ai-rules-generated-optional", - &generate_skill_content("optional", "Optional", "optional"), - ); - - let result = check_skills_in_sync(&source_files, temp_dir.path()).unwrap(); - assert!(result); - - create_skill_file(temp_dir.path(), "my-custom-skill", "custom"); - - let result = check_skills_in_sync(&source_files, temp_dir.path()).unwrap(); - assert!(result); - - create_skill_file(temp_dir.path(), "ai-rules-generated-orphaned", "orphaned"); - - let result = check_skills_in_sync(&source_files, temp_dir.path()).unwrap(); - assert!(!result); - } - - #[test] - fn test_check_skills_in_sync_wrong_content() { - let temp_dir = TempDir::new().unwrap(); - let source_files = vec![create_test_source_file( - "optional", "Optional", false, "Content", - )]; - - create_skill_file( - temp_dir.path(), - "ai-rules-generated-optional", - "---\nname: Wrong\ndescription: Wrong\n---\n\n@wrong.md", - ); - - let result = check_skills_in_sync(&source_files, temp_dir.path()).unwrap(); - assert!(!result); - } - - #[test] - fn test_check_skills_in_sync_different_folder_names() { - let temp_dir = TempDir::new().unwrap(); - - let source_files = vec![ - create_test_source_file("skill1", "Skill 1", false, "Content 1"), - create_test_source_file("skill3", "Skill 3", false, "Content 3"), - ]; - - create_skill_file( - temp_dir.path(), - "ai-rules-generated-skill1", - &generate_skill_content("skill-1", "Skill 1", "skill1"), - ); - create_skill_file( - temp_dir.path(), - "ai-rules-generated-skill2", - &generate_skill_content("skill-2", "Skill 2", "skill2"), - ); - - let result = check_skills_in_sync(&source_files, temp_dir.path()).unwrap(); - assert!(!result); - } - - #[test] - fn test_check_skills_in_sync_ignores_symlinks() { - let temp_dir = TempDir::new().unwrap(); - let source_files = vec![create_test_source_file( - "optional", "Optional", false, "Content", - )]; - - create_skill_file( - temp_dir.path(), - "ai-rules-generated-optional", - &generate_skill_content("optional", "Optional", "optional"), - ); - - let skills_dir = temp_dir.path().join(CLAUDE_SKILLS_DIR); - let symlink_target = temp_dir.path().join("fake-skill"); - std::fs::create_dir_all(&symlink_target).unwrap(); - std::os::unix::fs::symlink( - &symlink_target, - skills_dir.join("ai-rules-generated-symlink"), - ) - .unwrap(); - - assert!(check_skills_in_sync(&source_files, temp_dir.path()).unwrap()); - } -} diff --git a/src/operations/gitignore_updater.rs b/src/operations/gitignore_updater.rs index 037f883..828ac23 100644 --- a/src/operations/gitignore_updater.rs +++ b/src/operations/gitignore_updater.rs @@ -264,7 +264,7 @@ build/ *.other"#; fs::write(&gitignore_path, existing_content).unwrap(); - let registry = AgentToolRegistry::new(false); + let registry = AgentToolRegistry::new(); remove_gitignore_section(temp_path, ®istry).unwrap(); let content = fs::read_to_string(&gitignore_path).unwrap(); @@ -287,7 +287,7 @@ build/ let existing_content = "# Existing content\n*.old\n"; fs::write(&gitignore_path, existing_content).unwrap(); - let registry = AgentToolRegistry::new(false); + let registry = AgentToolRegistry::new(); remove_gitignore_section(temp_path, ®istry).unwrap(); let content = fs::read_to_string(&gitignore_path).unwrap(); @@ -296,7 +296,7 @@ build/ #[test] fn test_gitignore_includes_skill_patterns() { - let registry = AgentToolRegistry::new(false); + let registry = AgentToolRegistry::new(); let patterns = collect_all_gitignore_patterns(®istry, 1); // Check that skill patterns are included for agents that support skills diff --git a/src/operations/mod.rs b/src/operations/mod.rs index a8b2cf1..63c1de9 100644 --- a/src/operations/mod.rs +++ b/src/operations/mod.rs @@ -1,5 +1,4 @@ pub mod body_generator; -pub mod claude_skills; pub mod cleaner; pub mod command_reader; pub mod generation_result; @@ -10,9 +9,7 @@ pub mod optional_rules; pub mod skills_reader; pub mod source_reader; -pub use body_generator::{ - generate_all_rule_references, generate_body_contents, generate_inlined_required_content, -}; +pub use body_generator::{generate_all_rule_references, generate_body_contents}; pub use cleaner::clean_generated_files; #[allow(unused_imports)] pub use command_reader::{find_command_files, CommandFile}; diff --git a/src/operations/skills_reader.rs b/src/operations/skills_reader.rs index db10bd4..6246ea9 100644 --- a/src/operations/skills_reader.rs +++ b/src/operations/skills_reader.rs @@ -100,7 +100,7 @@ pub fn create_skill_symlinks(current_dir: &Path, target_dir: &str) -> Result Result<()> { let target_path = current_dir.join(target_dir); @@ -114,12 +114,14 @@ pub fn remove_generated_skill_symlinks(current_dir: &Path, target_dir: &str) -> let path = entry.path(); if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) { - // Remove any file/symlink that starts with our generated prefix. - // Note: fs::remove_file only works on files and symlinks, not directories, - // so this won't accidentally remove directories from experimental claude skills. if file_name.starts_with(GENERATED_FILE_PREFIX) { - fs::remove_file(&path) - .with_context(|| format!("Failed to remove: {}", path.display()))?; + if path.is_dir() && !path.is_symlink() { + fs::remove_dir_all(&path) + .with_context(|| format!("Failed to remove: {}", path.display()))?; + } else { + fs::remove_file(&path) + .with_context(|| format!("Failed to remove: {}", path.display()))?; + } } } } @@ -371,6 +373,31 @@ mod tests { assert_eq!(content, "user content"); } + #[test] + fn test_remove_generated_skill_directories() { + let temp_dir = TempDir::new().unwrap(); + let target_dir = temp_dir.path().join(".claude/skills"); + fs::create_dir_all(&target_dir).unwrap(); + + // Create a real generated directory (from old use_claude_skills mode) + let generated_dir = target_dir.join(format!("{}old-skill", GENERATED_FILE_PREFIX)); + fs::create_dir_all(&generated_dir).unwrap(); + fs::write(generated_dir.join(SKILL_FILENAME), "old content").unwrap(); + + // Create a user's custom skill directory + let user_skill = target_dir.join("user-custom-skill"); + fs::create_dir_all(&user_skill).unwrap(); + fs::write(user_skill.join(SKILL_FILENAME), "user content").unwrap(); + + remove_generated_skill_symlinks(temp_dir.path(), ".claude/skills").unwrap(); + + // Generated directory should be removed + assert!(!generated_dir.exists()); + + // User skill should remain + assert!(user_skill.exists()); + } + #[test] fn test_check_skill_symlinks_in_sync() { let temp_dir = TempDir::new().unwrap(); From 2fb0fe2762271b67b121b138ce654e213ca29bfe Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Thu, 26 Mar 2026 15:10:51 +1100 Subject: [PATCH 2/5] removed unnecessary deadcode annotation --- src/agents/external_skills_generator.rs | 12 ------------ src/agents/firebender.rs | 7 ------- src/agents/roo.rs | 3 --- src/agents/rule_generator.rs | 1 - src/agents/skills_generator.rs | 2 -- src/constants.rs | 2 -- src/operations/legacy_cleaner.rs | 4 ---- src/operations/skills_reader.rs | 6 ------ 8 files changed, 37 deletions(-) diff --git a/src/agents/external_skills_generator.rs b/src/agents/external_skills_generator.rs index 7b270d6..0add4cf 100644 --- a/src/agents/external_skills_generator.rs +++ b/src/agents/external_skills_generator.rs @@ -6,13 +6,11 @@ use crate::operations::skills_reader::{ use anyhow::Result; use std::path::{Path, PathBuf}; -#[allow(dead_code)] pub struct ExternalSkillsGenerator { target_dir: String, } impl ExternalSkillsGenerator { - #[allow(dead_code)] pub fn new(target_dir: &str) -> Self { Self { target_dir: target_dir.to_string(), @@ -21,10 +19,6 @@ impl ExternalSkillsGenerator { } impl SkillsGeneratorTrait for ExternalSkillsGenerator { - fn skills_target_dir(&self) -> &str { - &self.target_dir - } - fn generate_skills(&self, current_dir: &Path) -> Result> { create_skill_symlinks(current_dir, &self.target_dir) } @@ -59,12 +53,6 @@ mod tests { skill_dir } - #[test] - fn test_external_skills_generator_target_dir() { - let generator = ExternalSkillsGenerator::new(".claude/skills"); - assert_eq!(generator.skills_target_dir(), ".claude/skills"); - } - #[test] fn test_external_skills_generator_generate() { let temp_dir = TempDir::new().unwrap(); diff --git a/src/agents/firebender.rs b/src/agents/firebender.rs index 5449e23..654df4a 100644 --- a/src/agents/firebender.rs +++ b/src/agents/firebender.rs @@ -1017,13 +1017,6 @@ Create a git commit with proper formatting."#; assert!(generator.skills_generator().is_some()); } - #[test] - fn test_firebender_skills_target_dir() { - let generator = FirebenderGenerator; - let skills_gen = generator.skills_generator().unwrap(); - assert_eq!(skills_gen.skills_target_dir(), ".firebender/skills"); - } - #[test] fn test_firebender_skills_gitignore_patterns() { let generator = FirebenderGenerator; diff --git a/src/agents/roo.rs b/src/agents/roo.rs index b0b2748..8144471 100644 --- a/src/agents/roo.rs +++ b/src/agents/roo.rs @@ -7,16 +7,13 @@ use anyhow::Result; use std::collections::HashMap; use std::path::{Path, PathBuf}; -#[allow(dead_code)] // Used in Phase 3 when integrated into registry const ROO_DIR: &str = ".roo"; /// Roo generator that uses AGENTS.md (via SingleFileBasedGenerator) with MCP support -#[allow(dead_code)] // Used in Phase 3 when integrated into registry pub struct RooGenerator { inner: SingleFileBasedGenerator, } -#[allow(dead_code)] // Used in Phase 3 when integrated into registry impl RooGenerator { pub fn new() -> Self { Self { diff --git a/src/agents/rule_generator.rs b/src/agents/rule_generator.rs index 3fa619a..b387b0b 100644 --- a/src/agents/rule_generator.rs +++ b/src/agents/rule_generator.rs @@ -50,7 +50,6 @@ pub trait AgentRuleGenerator { /// /// Agents that support skills (like Claude, Codex, AMP) should override this /// to return their skills generator. The default returns `None` (no skills support). - #[allow(dead_code)] fn skills_generator(&self) -> Option> { None } diff --git a/src/agents/skills_generator.rs b/src/agents/skills_generator.rs index 16fa797..7a9d54e 100644 --- a/src/agents/skills_generator.rs +++ b/src/agents/skills_generator.rs @@ -1,9 +1,7 @@ use anyhow::Result; use std::path::{Path, PathBuf}; -#[allow(dead_code)] pub trait SkillsGeneratorTrait { - fn skills_target_dir(&self) -> &str; fn generate_skills(&self, current_dir: &Path) -> Result>; fn clean_skills(&self, current_dir: &Path) -> Result<()>; fn check_skills(&self, current_dir: &Path) -> Result; diff --git a/src/constants.rs b/src/constants.rs index c0d5062..4c58e41 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -9,9 +9,7 @@ pub const GENERATED_FILE_PREFIX: &str = "ai-rules-generated-"; pub const GENERATED_COMMAND_SUFFIX: &str = "ai-rules"; pub const CLAUDE_SKILLS_DIR: &str = ".claude/skills"; -#[allow(dead_code)] pub const CODEX_SKILLS_DIR: &str = ".codex/skills"; -#[allow(dead_code)] pub const AMP_SKILLS_DIR: &str = ".agents/skills"; pub const CURSOR_SKILLS_DIR: &str = ".cursor/skills"; pub const FIREBENDER_SKILLS_DIR: &str = ".firebender/skills"; diff --git a/src/operations/legacy_cleaner.rs b/src/operations/legacy_cleaner.rs index 54f159b..737b333 100644 --- a/src/operations/legacy_cleaner.rs +++ b/src/operations/legacy_cleaner.rs @@ -5,7 +5,6 @@ use std::path::Path; /// Legacy directory configurations for agents that migrated to AGENTS.md /// Each entry is (agent_dir, optional_rules_subdir) -#[allow(dead_code)] const LEGACY_AGENT_DIRS: &[(&str, Option<&str>)] = &[ (".roo", Some("rules")), (".clinerules", None), @@ -14,7 +13,6 @@ const LEGACY_AGENT_DIRS: &[(&str, Option<&str>)] = &[ /// Cleans up legacy generated files from agents that have migrated to AGENTS.md. /// Only removes files with the ai-rules-generated- prefix, then removes empty directories. -#[allow(dead_code)] pub fn clean_legacy_agent_directories(current_dir: &Path) -> Result<()> { for (agent_dir, rules_subdir) in LEGACY_AGENT_DIRS { let rules_path = if let Some(subdir) = rules_subdir { @@ -39,7 +37,6 @@ pub fn clean_legacy_agent_directories(current_dir: &Path) -> Result<()> { } /// Removes files with the ai-rules-generated- prefix from a directory -#[allow(dead_code)] fn remove_generated_files_from_directory(dir: &Path) -> Result<()> { if !dir.exists() { return Ok(()); @@ -62,7 +59,6 @@ fn remove_generated_files_from_directory(dir: &Path) -> Result<()> { } /// Removes a directory only if it's empty -#[allow(dead_code)] fn remove_directory_if_empty(dir: &Path) -> Result<()> { if !dir.exists() { return Ok(()); diff --git a/src/operations/skills_reader.rs b/src/operations/skills_reader.rs index 6246ea9..bcdd1eb 100644 --- a/src/operations/skills_reader.rs +++ b/src/operations/skills_reader.rs @@ -5,7 +5,6 @@ use std::path::{Path, PathBuf}; use crate::constants::{AI_RULE_SOURCE_DIR, GENERATED_FILE_PREFIX, SKILLS_DIR, SKILL_FILENAME}; use crate::utils::file_utils::{calculate_relative_path, create_relative_symlink}; -#[allow(dead_code)] #[derive(Debug, Clone)] pub struct SkillFolder { pub name: String, @@ -14,7 +13,6 @@ pub struct SkillFolder { } /// Finds all valid skill folders in ai-rules/skills/ directory -#[allow(dead_code)] pub fn find_skill_folders(current_dir: &Path) -> Result> { let skills_dir = current_dir.join(AI_RULE_SOURCE_DIR).join(SKILLS_DIR); @@ -72,7 +70,6 @@ pub fn find_skill_folders(current_dir: &Path) -> Result> { } /// Creates symlinks for each skill folder in the target directory -#[allow(dead_code)] pub fn create_skill_symlinks(current_dir: &Path, target_dir: &str) -> Result> { let skill_folders = find_skill_folders(current_dir)?; @@ -101,7 +98,6 @@ pub fn create_skill_symlinks(current_dir: &Path, target_dir: &str) -> Result Result<()> { let target_path = current_dir.join(target_dir); @@ -130,7 +126,6 @@ pub fn remove_generated_skill_symlinks(current_dir: &Path, target_dir: &str) -> } /// Checks if generated skill symlinks are in sync -#[allow(dead_code)] pub fn check_skill_symlinks_in_sync(current_dir: &Path, target_dir: &str) -> Result { let skill_folders = find_skill_folders(current_dir)?; let target_path = current_dir.join(target_dir); @@ -215,7 +210,6 @@ pub fn check_skill_symlinks_in_sync(current_dir: &Path, target_dir: &str) -> Res } /// Returns gitignore patterns for generated skill symlinks -#[allow(dead_code)] pub fn get_skill_gitignore_patterns(target_dir: &str) -> Vec { vec![format!("{}/{}*", target_dir, GENERATED_FILE_PREFIX)] } From 3e7fbecbec235d383012ee2407640af83444492f Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Thu, 26 Mar 2026 15:12:29 +1100 Subject: [PATCH 3/5] removed deadcode --- src/utils/frontmatter.rs | 106 --------------------------------------- src/utils/mod.rs | 1 - 2 files changed, 107 deletions(-) delete mode 100644 src/utils/frontmatter.rs diff --git a/src/utils/frontmatter.rs b/src/utils/frontmatter.rs deleted file mode 100644 index 7eea86a..0000000 --- a/src/utils/frontmatter.rs +++ /dev/null @@ -1,106 +0,0 @@ -use serde::de::DeserializeOwned; - -/// YAML frontmatter delimiter -#[allow(dead_code)] -pub const FRONTMATTER_DELIMITER: &str = "---"; - -/// Result of parsing frontmatter from content -#[allow(dead_code)] -#[derive(Debug, Clone)] -pub struct ParsedContent { - pub frontmatter: Option, - pub body: String, - pub raw_content: String, -} - -/// Splits content into raw frontmatter string and body, without parsing YAML. -/// Returns (frontmatter_str, body) if frontmatter exists, otherwise (None, original content). -#[allow(dead_code)] -pub fn split_frontmatter(content: &str) -> (Option<&str>, &str) { - let trimmed = content.trim_start(); - - if !trimmed.starts_with(FRONTMATTER_DELIMITER) { - return (None, content); - } - - let mut parts = trimmed.splitn(3, FRONTMATTER_DELIMITER); - parts.next(); // Skip empty string before first --- - - let frontmatter_str = match parts.next() { - Some(s) => s.trim(), - None => return (None, content), - }; - - let body = match parts.next() { - Some(s) => s.trim_start(), - None => return (None, content), - }; - - (Some(frontmatter_str), body) -} - -/// Parses content with optional YAML frontmatter into a typed struct. -/// Returns ParsedContent with frontmatter (if valid YAML) and body. -#[allow(dead_code)] -pub fn parse_frontmatter(content: &str) -> ParsedContent { - let (frontmatter_str, body) = split_frontmatter(content); - - let frontmatter = frontmatter_str.and_then(|s| serde_yaml::from_str(s).ok()); - - ParsedContent { - frontmatter, - body: body.to_string(), - raw_content: content.to_string(), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde::Deserialize; - - #[derive(Debug, Deserialize, PartialEq)] - struct TestFrontMatter { - title: String, - #[serde(default)] - enabled: bool, - } - - #[test] - fn test_split_frontmatter_with_frontmatter() { - let content = "---\ntitle: Test\n---\nBody content"; - let (fm, body) = split_frontmatter(content); - assert_eq!(fm, Some("title: Test")); - assert_eq!(body, "Body content"); - } - - #[test] - fn test_split_frontmatter_without_frontmatter() { - let content = "Just body content"; - let (fm, body) = split_frontmatter(content); - assert_eq!(fm, None); - assert_eq!(body, "Just body content"); - } - - #[test] - fn test_parse_frontmatter_typed() { - let content = "---\ntitle: Hello\nenabled: true\n---\nBody here"; - let parsed: ParsedContent = parse_frontmatter(content); - assert_eq!( - parsed.frontmatter, - Some(TestFrontMatter { - title: "Hello".to_string(), - enabled: true - }) - ); - assert_eq!(parsed.body, "Body here"); - } - - #[test] - fn test_parse_frontmatter_invalid_yaml() { - let content = "---\ninvalid: [unclosed\n---\nBody"; - let parsed: ParsedContent = parse_frontmatter(content); - assert_eq!(parsed.frontmatter, None); - assert_eq!(parsed.body, "Body"); - } -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 31534bf..6fbd595 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,6 +1,5 @@ pub mod dir_filter; pub mod file_utils; -pub mod frontmatter; pub mod git_utils; pub mod goose_utils; pub mod print_utils; From 0c779327c3125d898c879ce6d2307b9c028de381 Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Thu, 26 Mar 2026 16:29:31 +1100 Subject: [PATCH 4/5] clean up claude generator --- src/agents/claude.rs | 49 ++++++++----------------- src/agents/external_skills_generator.rs | 4 +- src/agents/registry.rs | 5 +-- src/operations/mod.rs | 2 +- src/operations/skills_reader.rs | 9 ++--- 5 files changed, 23 insertions(+), 46 deletions(-) diff --git a/src/agents/claude.rs b/src/agents/claude.rs index d965922..314e28d 100644 --- a/src/agents/claude.rs +++ b/src/agents/claude.rs @@ -3,19 +3,20 @@ use crate::agents::external_commands_generator::ExternalCommandsGenerator; use crate::agents::external_skills_generator::ExternalSkillsGenerator; use crate::agents::mcp_generator::{ExternalMcpGenerator, McpGeneratorTrait}; use crate::agents::rule_generator::AgentRuleGenerator; +use crate::agents::single_file_based::{ + check_in_sync, clean_generated_files, generate_agent_file_contents, +}; use crate::agents::skills_generator::SkillsGeneratorTrait; use crate::constants::{ CLAUDE_COMMANDS_DIR, CLAUDE_COMMANDS_SUBDIR, CLAUDE_MCP_JSON, CLAUDE_SKILLS_DIR, }; use crate::models::source_file::SourceFile; -use crate::operations::remove_generated_skill_symlinks; use crate::utils::file_utils::{ check_agents_md_symlink, check_inlined_file_symlink, create_symlink_to_agents_md, create_symlink_to_inlined_file, }; use anyhow::Result; use std::collections::HashMap; -use std::fs; use std::path::{Path, PathBuf}; pub struct ClaudeGenerator { @@ -38,21 +39,15 @@ impl AgentRuleGenerator for ClaudeGenerator { } fn clean(&self, current_dir: &Path) -> Result<()> { - let output_file = current_dir.join(&self.output_filename); - if output_file.exists() || output_file.is_symlink() { - fs::remove_file(&output_file)?; - } - remove_generated_skill_symlinks(current_dir, CLAUDE_SKILLS_DIR)?; - - Ok(()) + clean_generated_files(current_dir, &self.output_filename) } fn generate_agent_contents( &self, - _source_files: &[SourceFile], - _current_dir: &Path, + source_files: &[SourceFile], + current_dir: &Path, ) -> HashMap { - HashMap::new() + generate_agent_file_contents(source_files, current_dir, &self.output_filename) } fn check_agent_contents( @@ -60,13 +55,7 @@ impl AgentRuleGenerator for ClaudeGenerator { source_files: &[SourceFile], current_dir: &Path, ) -> Result { - if source_files.is_empty() { - let file_path = current_dir.join(&self.output_filename); - if file_path.exists() { - return Ok(false); - } - } - Ok(true) + check_in_sync(source_files, current_dir, &self.output_filename) } fn check_symlink(&self, current_dir: &Path) -> Result { @@ -131,27 +120,15 @@ mod tests { use tempfile::TempDir; #[test] - fn test_clean_removes_both_file_and_skills() { + fn test_clean_removes_output_file() { let temp_dir = TempDir::new().unwrap(); let generator = ClaudeGenerator::new("claude", "CLAUDE.md"); create_file(temp_dir.path(), "CLAUDE.md", "content"); - let generated_skills_dir = temp_dir - .path() - .join(".claude/skills/ai-rules-generated-test"); - std::fs::create_dir_all(&generated_skills_dir).unwrap(); - std::fs::write(generated_skills_dir.join("SKILL.md"), "generated skill").unwrap(); - - let user_skills_dir = temp_dir.path().join(".claude/skills/my-custom-skill"); - std::fs::create_dir_all(&user_skills_dir).unwrap(); - std::fs::write(user_skills_dir.join("SKILL.md"), "user skill").unwrap(); - generator.clean(temp_dir.path()).unwrap(); assert!(!temp_dir.path().join("CLAUDE.md").exists()); - assert!(!generated_skills_dir.exists()); - assert!(user_skills_dir.exists()); } #[test] @@ -164,7 +141,7 @@ mod tests { } #[test] - fn test_generate_agent_contents_returns_empty() { + fn test_generate_agent_contents() { let temp_dir = TempDir::new().unwrap(); let generator = ClaudeGenerator::new("claude", "CLAUDE.md"); let source_files = vec![ @@ -186,6 +163,10 @@ mod tests { let files = generator.generate_agent_contents(&source_files, temp_dir.path()); - assert_eq!(files.len(), 0); + assert_eq!(files.len(), 1); + let expected_path = temp_dir.path().join("CLAUDE.md"); + let content = files.get(&expected_path).unwrap(); + assert!(content.contains("@ai-rules/.generated-ai-rules/ai-rules-generated-always1.md")); + assert!(content.contains("@ai-rules/.generated-ai-rules/ai-rules-generated-optional.md")); } } diff --git a/src/agents/external_skills_generator.rs b/src/agents/external_skills_generator.rs index 0add4cf..b8873d0 100644 --- a/src/agents/external_skills_generator.rs +++ b/src/agents/external_skills_generator.rs @@ -1,7 +1,7 @@ use crate::agents::skills_generator::SkillsGeneratorTrait; use crate::operations::skills_reader::{ check_skill_symlinks_in_sync, create_skill_symlinks, get_skill_gitignore_patterns, - remove_generated_skill_symlinks, + remove_generated_skills, }; use anyhow::Result; use std::path::{Path, PathBuf}; @@ -24,7 +24,7 @@ impl SkillsGeneratorTrait for ExternalSkillsGenerator { } fn clean_skills(&self, current_dir: &Path) -> Result<()> { - remove_generated_skill_symlinks(current_dir, &self.target_dir) + remove_generated_skills(current_dir, &self.target_dir) } fn check_skills(&self, current_dir: &Path) -> Result { diff --git a/src/agents/registry.rs b/src/agents/registry.rs index dd926b8..c896d6a 100644 --- a/src/agents/registry.rs +++ b/src/agents/registry.rs @@ -15,11 +15,8 @@ impl AgentToolRegistry { pub fn new() -> Self { let mut tools: HashMap> = HashMap::new(); - let claude_generator: Box = - Box::new(ClaudeGenerator::new("claude", "CLAUDE.md")); - let generators: Vec> = vec![ - claude_generator, + Box::new(ClaudeGenerator::new("claude", "CLAUDE.md")), Box::new(SingleFileBasedGenerator::new("cline", AGENTS_MD_FILENAME)), Box::new(CursorGenerator::new()), Box::new(FirebenderGenerator), diff --git a/src/operations/mod.rs b/src/operations/mod.rs index 63c1de9..7cf018a 100644 --- a/src/operations/mod.rs +++ b/src/operations/mod.rs @@ -20,6 +20,6 @@ pub use legacy_cleaner::clean_legacy_agent_directories; #[allow(unused_imports)] pub use skills_reader::{ check_skill_symlinks_in_sync, create_skill_symlinks, find_skill_folders, - get_skill_gitignore_patterns, remove_generated_skill_symlinks, SkillFolder, + get_skill_gitignore_patterns, remove_generated_skills, SkillFolder, }; pub use source_reader::find_source_files; diff --git a/src/operations/skills_reader.rs b/src/operations/skills_reader.rs index bcdd1eb..4d5e235 100644 --- a/src/operations/skills_reader.rs +++ b/src/operations/skills_reader.rs @@ -97,8 +97,7 @@ pub fn create_skill_symlinks(current_dir: &Path, target_dir: &str) -> Result Result<()> { +pub fn remove_generated_skills(current_dir: &Path, target_dir: &str) -> Result<()> { let target_path = current_dir.join(target_dir); if !target_path.exists() { @@ -352,7 +351,7 @@ mod tests { fs::write(user_skill.join(SKILL_FILENAME), "user content").unwrap(); // Remove generated symlinks - remove_generated_skill_symlinks(temp_dir.path(), ".claude/skills").unwrap(); + remove_generated_skills(temp_dir.path(), ".claude/skills").unwrap(); // Check generated symlink is gone let generated = temp_dir @@ -383,7 +382,7 @@ mod tests { fs::create_dir_all(&user_skill).unwrap(); fs::write(user_skill.join(SKILL_FILENAME), "user content").unwrap(); - remove_generated_skill_symlinks(temp_dir.path(), ".claude/skills").unwrap(); + remove_generated_skills(temp_dir.path(), ".claude/skills").unwrap(); // Generated directory should be removed assert!(!generated_dir.exists()); @@ -554,7 +553,7 @@ mod tests { assert!(!broken_symlink.exists()); // exists() returns false for broken symlinks // Remove should clean up broken symlinks - remove_generated_skill_symlinks(temp_dir.path(), ".claude/skills").unwrap(); + remove_generated_skills(temp_dir.path(), ".claude/skills").unwrap(); // Broken symlink should be removed assert!(!broken_symlink.is_symlink()); From bb430d8bfdb3f04d657c321bdc4650ded386cd9e Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Thu, 26 Mar 2026 16:40:27 +1100 Subject: [PATCH 5/5] sync this project ai rules --- .cursor/rules/ai-rules-generated-index.mdc | 75 ------------------- .../ai-rules-generated-AGENTS.md | 8 ++ .../ai-rules-generated-index.md | 8 ++ 3 files changed, 16 insertions(+), 75 deletions(-) delete mode 100644 .cursor/rules/ai-rules-generated-index.mdc diff --git a/.cursor/rules/ai-rules-generated-index.mdc b/.cursor/rules/ai-rules-generated-index.mdc deleted file mode 100644 index f13d837..0000000 --- a/.cursor/rules/ai-rules-generated-index.mdc +++ /dev/null @@ -1,75 +0,0 @@ ---- -description: Base repository guidelines -alwaysApply: true ---- - -# Repository Guidelines - -This is a Rust-based CLI tool called `ai-rules` that manages AI rules across different AI coding agents. The project uses Cargo for dependency management and Hermit for environment tooling. - -## Project Structure & Module Organization - -The codebase follows a modular Rust structure: - -- `src/main.rs` - Entry point that delegates to CLI runner -- `src/cli/` - Command-line argument parsing with clap -- `src/commands/` - Command implementations (init, generate, list_agents, status, clean) -- `src/operations/` - Core business logic (body generation, gitignore updates, source reading) -- `src/agents/` - Agent-specific configurations and handlers -- `src/models/` - Data structures and types -- `src/utils/` - Utility functions (file, git, goose, print, prompt) -- `src/templates/` - Embedded template files for initialization -- `scripts/` - Shell scripts for CI tasks (clippy-check.sh, clippy-fix.sh, release.sh) -- `target/` - Build artifacts (gitignored) - -## Build, Test, and Development Commands - -**Build:** -- `cargo build` - Debug build -- `cargo build --release` - Optimized release build - -**Test:** -- `cargo test` - Run all tests -- `cargo test --verbose` - Run tests with detailed output -- `cargo test ` - Run specific test - -**Linting & Formatting:** -- `cargo fmt --all` - Format code -- `cargo fmt --check` - Check formatting without modifying -- `cargo clippy` - Run lints -- `./scripts/clippy-check.sh` - Run clippy with strict warnings (used in CI) -- `./scripts/clippy-fix.sh` - Auto-fix clippy issues - -**Security:** -- `cargo audit` - Check for security vulnerabilities in dependencies - -**Run the tool:** -- `cargo run -- ` - Run with arguments (e.g., `cargo run -- init`) - -## Coding Style & Naming Conventions - -**Code Organization:** -- Each module has a `mod.rs` that exports public items -- Tests are included in the same file using `#[cfg(test)]` modules -- Use `anyhow::Result` for error handling with context -- Prefer explicit error messages with `.with_context()` - -## Testing Guidelines - -**Framework:** Built-in Rust testing with `tempfile` for temporary directories - -**Test Organization:** -- Unit tests live in `#[cfg(test)]` modules at the bottom of each source file -- Test functions use `#[test]` attribute -- Use descriptive test names: `test__` (e.g., `test_load_config_no_file`) - -**Running Tests:** -- `cargo test` - All tests -- `cargo test ` - Tests in specific module -- `cargo test ` - Specific test - -**Test Patterns:** -- Use `tempfile::TempDir` for filesystem tests -- Use `unwrap()` in tests (it's acceptable to panic on test failures) -- Test both success and error cases -- Include edge cases (empty inputs, invalid data, missing files) diff --git a/ai-rules/.generated-ai-rules/ai-rules-generated-AGENTS.md b/ai-rules/.generated-ai-rules/ai-rules-generated-AGENTS.md index 46b88dc..9fabf82 100644 --- a/ai-rules/.generated-ai-rules/ai-rules-generated-AGENTS.md +++ b/ai-rules/.generated-ai-rules/ai-rules-generated-AGENTS.md @@ -43,6 +43,14 @@ The codebase follows a modular Rust structure: **Run the tool:** - `cargo run -- ` - Run with arguments (e.g., `cargo run -- init`) +## Before Committing + +Always run these before committing changes: +1. `cargo fmt --all` - Format code +2. `./scripts/clippy-check.sh` - Check for lint errors + +Do not commit code that fails formatting or clippy checks. + ## Coding Style & Naming Conventions **Code Organization:** diff --git a/ai-rules/.generated-ai-rules/ai-rules-generated-index.md b/ai-rules/.generated-ai-rules/ai-rules-generated-index.md index 73f10ba..eec0af0 100644 --- a/ai-rules/.generated-ai-rules/ai-rules-generated-index.md +++ b/ai-rules/.generated-ai-rules/ai-rules-generated-index.md @@ -41,6 +41,14 @@ The codebase follows a modular Rust structure: **Run the tool:** - `cargo run -- ` - Run with arguments (e.g., `cargo run -- init`) +## Before Committing + +Always run these before committing changes: +1. `cargo fmt --all` - Format code +2. `./scripts/clippy-check.sh` - Check for lint errors + +Do not commit code that fails formatting or clippy checks. + ## Coding Style & Naming Conventions **Code Organization:**