From 81b0df1ae42869c212bf999dd91459dd8e1de9f1 Mon Sep 17 00:00:00 2001 From: Jon Andersen Date: Wed, 25 Mar 2026 11:03:33 -0400 Subject: [PATCH 1/4] Use AGENTS.md by default for Cursor Switch Cursor to the shared AGENTS.md path by default while preserving a config opt-out for legacy .cursor/rules output. Update clean, status, examples, and docs so stale Cursor files are handled correctly in both modes. Signed-off-by: Jon Andersen Made-with: Cursor --- Example/.gitignore | 1 - Example/ai-rules/ai-rules-config.yaml | 1 + README.md | 4 +- docs/agents.md | 2 +- docs/configuration.md | 12 ++ docs/project-structure.md | 4 +- docs/rule-format.md | 4 +- src/agents/cursor.rs | 252 +++++++++++++++++++------- src/agents/registry.rs | 4 +- src/cli/mod.rs | 27 ++- src/cli/tests.rs | 10 + src/commands/clean.rs | 56 ++++-- src/commands/generate.rs | 111 ++++++------ src/commands/list_agents.rs | 4 +- src/commands/mod.rs | 17 +- src/commands/status.rs | 62 ++++--- src/config.rs | 66 +++++++ src/operations/gitignore_updater.rs | 6 +- 18 files changed, 467 insertions(+), 176 deletions(-) diff --git a/Example/.gitignore b/Example/.gitignore index ad92b22..4397d0e 100644 --- a/Example/.gitignore +++ b/Example/.gitignore @@ -7,7 +7,6 @@ **/.codex/skills/ai-rules-generated-* **/.cursor/commands/ai-rules/ **/.cursor/mcp.json -**/.cursor/rules/ **/.cursor/skills/ai-rules-generated-* **/.firebender/skills/ai-rules-generated-* **/.gemini/settings.json diff --git a/Example/ai-rules/ai-rules-config.yaml b/Example/ai-rules/ai-rules-config.yaml index 7edcec5..f2dc489 100644 --- a/Example/ai-rules/ai-rules-config.yaml +++ b/Example/ai-rules/ai-rules-config.yaml @@ -1,3 +1,4 @@ agents: ["claude", "cursor", "firebender", "goose", "amp", "cline", "roo", "kilocode"] gitignore: true nested_depth: 2 +use_cursor_rules: false diff --git a/README.md b/README.md index 9727bfa..6a3e502 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ curl -fsSL https://raw.githubusercontent.com/block/ai-rules/main/scripts/install ai-rules generate # All agents ai-rules generate --agents claude,cursor # Specific agents ``` - Creates `CLAUDE.md`, `.cursor/rules/*.mdc`, `AGENTS.md`, etc. + Creates `CLAUDE.md`, `AGENTS.md`, and other agent-specific files. Cursor uses `AGENTS.md` by default; set `use_cursor_rules: true` to keep the legacy `.cursor/rules/*.mdc` output. 4. **Check status** to ensure everything is in sync: ```bash @@ -79,6 +79,8 @@ Create `ai-rules/ai-rules-config.yaml` to set defaults: agents: [claude, cursor, cline] nested_depth: 2 gitignore: true +# Optional: keep legacy Cursor `.cursor/rules/*.mdc` output instead of `AGENTS.md` +# use_cursor_rules: true ``` See [Configuration Guide](docs/configuration.md) for all options. diff --git a/docs/agents.md b/docs/agents.md index f4a1920..8224ecb 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -9,7 +9,7 @@ | **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` | - | | -| **Cursor** | `.cursor/rules/*.mdc` | `AGENTS.md` -> `ai-rules/AGENTS.md` | `.cursor/mcp.json` | Symlink mode: only project root level | +| **Cursor** | `AGENTS.md` -> inlined file | `AGENTS.md` -> `ai-rules/AGENTS.md` | `.cursor/mcp.json` | Set `use_cursor_rules: true` to keep legacy `.cursor/rules/*.mdc` output | | **Firebender** | `firebender.json` | `firebender.json` (references `ai-rules/AGENTS.md`) | Embedded in `firebender.json` | Supports overlay files | | **Gemini** | `GEMINI.md` -> inlined file | `GEMINI.md` -> `ai-rules/AGENTS.md` | Embedded in `.gemini/settings.json` | | | **Goose** | `AGENTS.md` -> inlined file | `AGENTS.md` -> `ai-rules/AGENTS.md` | - | | diff --git a/docs/configuration.md b/docs/configuration.md index 2448423..55131ca 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -11,6 +11,8 @@ agents: [claude, cursor, cline] # Generate rules only for these agents command_agents: [claude, amp] # Generate commands for these agents (defaults to agents list if not specified) nested_depth: 2 # Search 2 levels deep for ai-rules/ folders gitignore: true # Ignore the generated rules in git +# Optional: keep legacy Cursor `.cursor/rules/*.mdc` output instead of `AGENTS.md` +# use_cursor_rules: true ``` ## Configuration Precedence @@ -32,3 +34,13 @@ 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. + +### Cursor Legacy Rules Mode + +```yaml +use_cursor_rules: true # Default: false +``` + +Cursor now uses `AGENTS.md` by default in standard mode so it shares the same generated content path as other `AGENTS.md`-based agents. Enable `use_cursor_rules` to opt out and keep the legacy `.cursor/rules/*.mdc` output. + +This opt-out is useful if you still rely on Cursor-specific `.mdc` metadata such as `fileMatching`/`globs`. diff --git a/docs/project-structure.md b/docs/project-structure.md index c5572e4..e6e5890 100644 --- a/docs/project-structure.md +++ b/docs/project-structure.md @@ -26,7 +26,6 @@ monorepo/ │ │ └── styling.md │ ├── CLAUDE.md # Symlink -> ai-rules/.generated-ai-rules/ai-rules-generated-AGENTS.md │ ├── AGENTS.md # Symlink -> ai-rules/.generated-ai-rules/ai-rules-generated-AGENTS.md -│ ├── .cursor/rules/ # Generated (*.mdc) │ └── src/ │ ├── backend/ # Backend services @@ -40,12 +39,13 @@ monorepo/ │ ├── CLAUDE.md # Symlink -> ai-rules/.generated-ai-rules/ai-rules-generated-AGENTS.md ├── AGENTS.md # Symlink -> ai-rules/.generated-ai-rules/ai-rules-generated-AGENTS.md -├── .cursor/rules/ # Root Cursor rules (generated *.mdc files) ├── .clinerules/ # Root Cline rules (symlink) ├── .mcp.json # Root MCP config (generated) └── firebender.json # Root Firebender config (generated) ``` +If you need Cursor's legacy `.cursor/rules/*.mdc` output instead, enable `use_cursor_rules: true` in `ai-rules/ai-rules-config.yaml`. + ## Symlink Mode For simple projects with a single `AGENTS.md` file: diff --git a/docs/rule-format.md b/docs/rule-format.md index 41c9c56..6a8b4c6 100644 --- a/docs/rule-format.md +++ b/docs/rule-format.md @@ -28,7 +28,7 @@ All fields are optional: |-------|-------------|---------| | `description` | Context description that helps agents understand when to apply this rule if `alwaysApply` is `false` | - | | `alwaysApply` | `true` = referenced directly in agent rule files; `false` = included as optional rules based on context | `true` | -| `fileMatching` | Glob patterns for which files this rule applies to (e.g., `"**/*.ts"`, `"src/**/*.py"`). Currently supported in Cursor. | - | +| `fileMatching` | Glob patterns for which files this rule applies to (e.g., `"**/*.ts"`, `"src/**/*.py"`). Currently supported when Cursor is using legacy `.cursor/rules/*.mdc` generation (`use_cursor_rules: true`). | - | If frontmatter is omitted entirely, the file is treated as a regular markdown rule with default settings (`alwaysApply: true`). @@ -38,7 +38,7 @@ In Standard Mode, `ai-rules generate` produces a single inlined file at `ai-rule Most agent output files (e.g., `CLAUDE.md`, `AGENTS.md`, `GEMINI.md`) are created as **symlinks** pointing to this inlined file. This ensures every agent reads the same complete content without needing `@` file expansion support. -Cursor and Firebender generate their own formats (`.mdc` files and JSON respectively) and do not use the inlined file. +Firebender generates its own JSON format and does not use the inlined file. Cursor uses the inlined `AGENTS.md` path by default, but can still generate legacy `.mdc` files when `use_cursor_rules: true`. ## Symlink Mode diff --git a/src/agents/cursor.rs b/src/agents/cursor.rs index 60b7978..24a3425 100644 --- a/src/agents/cursor.rs +++ b/src/agents/cursor.rs @@ -3,16 +3,14 @@ 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::SingleFileBasedGenerator; use crate::agents::skills_generator::SkillsGeneratorTrait; use crate::constants::{ AGENTS_MD_FILENAME, CURSOR_COMMANDS_DIR, CURSOR_COMMANDS_SUBDIR, CURSOR_SKILLS_DIR, GENERATED_FILE_PREFIX, MCP_JSON, }; use crate::models::SourceFile; -use crate::utils::file_utils::{ - check_agents_md_symlink, check_directory_exact_match, create_symlink_to_agents_md, - ensure_trailing_newline, -}; +use crate::utils::file_utils::{check_directory_exact_match, ensure_trailing_newline}; use anyhow::Result; use std::collections::HashMap; use std::fs; @@ -20,25 +18,86 @@ use std::path::{Path, PathBuf}; const MDC_EXTENSION: &str = "mdc"; -pub struct CursorGenerator; +pub struct CursorGenerator { + use_cursor_rules: bool, + single_file: SingleFileBasedGenerator, +} + +impl CursorGenerator { + pub fn new(use_cursor_rules: bool) -> Self { + Self { + use_cursor_rules, + single_file: SingleFileBasedGenerator::new("cursor", AGENTS_MD_FILENAME), + } + } +} + +impl Default for CursorGenerator { + fn default() -> Self { + Self::new(false) + } +} fn get_cursor_rules_dir(current_dir: &Path) -> PathBuf { current_dir.join(".cursor").join("rules") } +fn clean_cursor_rules_dir(current_dir: &Path) -> Result<()> { + let cursor_rules_dir = get_cursor_rules_dir(current_dir); + if cursor_rules_dir.exists() { + fs::remove_dir_all(cursor_rules_dir)?; + } + Ok(()) +} + +fn generate_cursor_rule_contents( + source_files: &[SourceFile], + current_dir: &Path, +) -> HashMap { + let mut agent_files = HashMap::new(); + + if source_files.is_empty() { + return agent_files; + } + + let cursor_rules_dir = get_cursor_rules_dir(current_dir); + + for source_file in source_files { + let generated_file_name = format!( + "{}{}.{}", + GENERATED_FILE_PREFIX, source_file.base_file_name, MDC_EXTENSION + ); + + let cursor_file_path = cursor_rules_dir.join(generated_file_name); + + if let Ok(content) = generate_rule_file_content(source_file) { + agent_files.insert(cursor_file_path, content); + } + } + + agent_files +} + +fn check_cursor_rule_contents(source_files: &[SourceFile], current_dir: &Path) -> Result { + let cursor_rules_dir = get_cursor_rules_dir(current_dir); + + if source_files.is_empty() { + return Ok(!cursor_rules_dir.exists()); + } + + let expected_files = generate_cursor_rule_contents(source_files, current_dir); + check_directory_exact_match(&cursor_rules_dir, &expected_files) +} + impl AgentRuleGenerator for CursorGenerator { fn name(&self) -> &str { "cursor" } fn clean(&self, current_dir: &Path) -> Result<()> { - let cursor_rules_dir = get_cursor_rules_dir(current_dir); - if cursor_rules_dir.exists() { - fs::remove_dir_all(cursor_rules_dir)?; - } - let agent_md = current_dir.join(AGENTS_MD_FILENAME); - if agent_md.exists() && agent_md.is_symlink() { - fs::remove_file(agent_md)?; + clean_cursor_rules_dir(current_dir)?; + if !self.use_cursor_rules { + self.single_file.clean(current_dir)?; } Ok(()) } @@ -48,28 +107,12 @@ impl AgentRuleGenerator for CursorGenerator { source_files: &[SourceFile], current_dir: &Path, ) -> HashMap { - let mut agent_files = HashMap::new(); - - if source_files.is_empty() { - return agent_files; - } - - let cursor_rules_dir = get_cursor_rules_dir(current_dir); - - for source_file in source_files { - let generated_file_name = format!( - "{}{}.{}", - GENERATED_FILE_PREFIX, source_file.base_file_name, MDC_EXTENSION - ); - - let cursor_file_path = cursor_rules_dir.join(generated_file_name); - - if let Ok(content) = generate_rule_file_content(source_file) { - agent_files.insert(cursor_file_path, content); - } + if self.use_cursor_rules { + generate_cursor_rule_contents(source_files, current_dir) + } else { + self.single_file + .generate_agent_contents(source_files, current_dir) } - - agent_files } fn check_agent_contents( @@ -77,33 +120,46 @@ impl AgentRuleGenerator for CursorGenerator { source_files: &[SourceFile], current_dir: &Path, ) -> Result { - let cursor_rules_dir = get_cursor_rules_dir(current_dir); - - if source_files.is_empty() { - return Ok(!cursor_rules_dir.exists()); + if self.use_cursor_rules { + check_cursor_rule_contents(source_files, current_dir) + } else { + Ok(self + .single_file + .check_agent_contents(source_files, current_dir)? + && !get_cursor_rules_dir(current_dir).exists()) } - - let expected_files = self.generate_agent_contents(source_files, current_dir); - - check_directory_exact_match(&cursor_rules_dir, &expected_files) } fn check_symlink(&self, current_dir: &Path) -> Result { - let agents_md_path = current_dir.join(AGENTS_MD_FILENAME); - check_agents_md_symlink(current_dir, &agents_md_path) + Ok(self.single_file.check_symlink(current_dir)? + && !get_cursor_rules_dir(current_dir).exists()) } fn gitignore_patterns(&self) -> Vec { - vec![".cursor/rules/".to_string()] + if self.use_cursor_rules { + vec![".cursor/rules/".to_string()] + } else { + self.single_file.gitignore_patterns() + } } fn generate_symlink(&self, current_dir: &Path) -> Result> { - let success = create_symlink_to_agents_md(current_dir, Path::new(AGENTS_MD_FILENAME))?; - if success { - Ok(vec![current_dir.join(AGENTS_MD_FILENAME)]) - } else { - Ok(vec![]) - } + clean_cursor_rules_dir(current_dir)?; + self.single_file.generate_symlink(current_dir) + } + + fn uses_inlined_symlink(&self) -> bool { + !self.use_cursor_rules + } + + fn generate_inlined_symlink(&self, current_dir: &Path) -> Result> { + clean_cursor_rules_dir(current_dir)?; + self.single_file.generate_inlined_symlink(current_dir) + } + + fn check_inlined_symlink(&self, current_dir: &Path) -> Result { + Ok(self.single_file.check_inlined_symlink(current_dir)? + && !get_cursor_rules_dir(current_dir).exists()) } fn mcp_generator(&self) -> Option> { @@ -217,8 +273,41 @@ alwaysApply: true } #[test] - fn test_generate_agent_contents() { - let generator = CursorGenerator; + fn test_default_cursor_gitignore_patterns_use_agents_md() { + let generator = CursorGenerator::default(); + let patterns = generator.gitignore_patterns(); + + assert_eq!(patterns, vec![AGENTS_MD_FILENAME.to_string()]); + } + + #[test] + fn test_legacy_cursor_gitignore_patterns_use_cursor_rules_dir() { + let generator = CursorGenerator::new(true); + let patterns = generator.gitignore_patterns(); + + assert_eq!(patterns, vec![".cursor/rules/".to_string()]); + } + + #[test] + fn test_generate_agent_contents_default_uses_agents_md() { + let generator = CursorGenerator::default(); + let temp_dir = TempDir::new().unwrap(); + let source_files = vec![create_standard_test_source_file()]; + + let result = generator.generate_agent_contents(&source_files, temp_dir.path()); + + let expected_path = temp_dir.path().join(AGENTS_MD_FILENAME); + let content = result.get(&expected_path).unwrap(); + + assert_eq!( + content, + "@ai-rules/.generated-ai-rules/ai-rules-generated-test.md\n" + ); + } + + #[test] + fn test_generate_agent_contents_legacy_cursor_rules_mode() { + let generator = CursorGenerator::new(true); let temp_dir = TempDir::new().unwrap(); let source_files = vec![ create_test_source_file( @@ -273,7 +362,7 @@ rule2 body #[test] fn test_clean_non_existing_directory() { - let generator = CursorGenerator; + let generator = CursorGenerator::default(); let temp_dir = TempDir::new().unwrap(); let result = generator.clean(temp_dir.path()); @@ -283,8 +372,8 @@ rule2 body } #[test] - fn test_clean_existing_directory() { - let generator = CursorGenerator; + fn test_clean_default_removes_existing_directory() { + let generator = CursorGenerator::default(); let temp_dir = TempDir::new().unwrap(); create_file( temp_dir.path(), @@ -312,8 +401,8 @@ rule2 body } #[test] - fn test_clean_removes_agents_md_symlink() { - let generator = CursorGenerator; + fn test_clean_default_removes_agents_md_symlink() { + let generator = CursorGenerator::default(); let temp_dir = TempDir::new().unwrap(); create_file(temp_dir.path(), "ai-rules/AGENTS.md", "# Source content"); @@ -332,9 +421,27 @@ rule2 body assert_file_exists(temp_dir.path(), "ai-rules/AGENTS.md"); } + #[test] + fn test_clean_legacy_preserves_existing_agents_md() { + let generator = CursorGenerator::new(true); + let temp_dir = TempDir::new().unwrap(); + + create_file(temp_dir.path(), AGENTS_MD_FILENAME, "shared agents content"); + create_file( + temp_dir.path(), + ".cursor/rules/ai-rules-generated-stale.mdc", + "stale content", + ); + + generator.clean(temp_dir.path()).unwrap(); + + assert_file_exists(temp_dir.path(), AGENTS_MD_FILENAME); + assert_file_not_exists(temp_dir.path(), ".cursor/rules"); + } + #[test] fn test_check_empty_source_files_with_directory() { - let generator = CursorGenerator; + let generator = CursorGenerator::new(true); let temp_dir = TempDir::new().unwrap(); create_file( @@ -352,7 +459,7 @@ rule2 body #[test] fn test_check_with_matching_files() { - let generator = CursorGenerator; + let generator = CursorGenerator::new(true); let temp_dir = TempDir::new().unwrap(); let source_file = create_standard_test_source_file(); @@ -371,7 +478,7 @@ rule2 body #[test] fn test_check_with_missing_files() { - let generator = CursorGenerator; + let generator = CursorGenerator::new(true); let temp_dir = TempDir::new().unwrap(); let source_file = create_standard_test_source_file(); @@ -384,7 +491,7 @@ rule2 body #[test] fn test_check_with_incorrect_content() { - let generator = CursorGenerator; + let generator = CursorGenerator::new(true); let temp_dir = TempDir::new().unwrap(); let source_file = create_standard_test_source_file(); @@ -403,7 +510,7 @@ rule2 body #[test] fn test_check_symlink_with_correct_symlink() { - let generator = CursorGenerator; + let generator = CursorGenerator::default(); let temp_dir = TempDir::new().unwrap(); create_file(temp_dir.path(), "ai-rules/AGENTS.md", "# Source content"); @@ -414,4 +521,25 @@ rule2 body let result = generator.check_symlink(temp_dir.path()).unwrap(); assert!(result); } + + #[test] + fn test_check_inlined_symlink_rejects_stale_cursor_rules_dir() { + let generator = CursorGenerator::default(); + let temp_dir = TempDir::new().unwrap(); + + create_file( + temp_dir.path(), + "ai-rules/.generated-ai-rules/ai-rules-generated-AGENTS.md", + "# Inlined content\n", + ); + generator.generate_inlined_symlink(temp_dir.path()).unwrap(); + create_file( + temp_dir.path(), + ".cursor/rules/ai-rules-generated-stale.mdc", + "stale content", + ); + + let result = generator.check_inlined_symlink(temp_dir.path()).unwrap(); + assert!(!result); + } } diff --git a/src/agents/registry.rs b/src/agents/registry.rs index 8552831..6512ace 100644 --- a/src/agents/registry.rs +++ b/src/agents/registry.rs @@ -12,7 +12,7 @@ pub struct AgentToolRegistry { } impl AgentToolRegistry { - pub fn new(use_claude_skills: bool) -> Self { + pub fn new(use_claude_skills: bool, use_cursor_rules: bool) -> Self { let mut tools: HashMap> = HashMap::new(); // Claude now always uses ClaudeGenerator with skills_mode parameter @@ -25,7 +25,7 @@ impl AgentToolRegistry { let generators: Vec> = vec![ claude_generator, Box::new(SingleFileBasedGenerator::new("cline", AGENTS_MD_FILENAME)), - Box::new(CursorGenerator), + Box::new(CursorGenerator::new(use_cursor_rules)), Box::new(FirebenderGenerator), Box::new(SingleFileBasedGenerator::new("goose", AGENTS_MD_FILENAME)), Box::new(AmpGenerator), diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 5a708f9..9b359f9 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -28,22 +28,41 @@ pub fn run_cli() -> anyhow::Result<()> { .as_ref() .and_then(|c| c.use_claude_skills) .unwrap_or(false); + let use_cursor_rules = config + .as_ref() + .and_then(|c| c.use_cursor_rules) + .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, + use_claude_skills, + use_cursor_rules, + ) } 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, + use_claude_skills, + use_cursor_rules, + ) } 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, + use_claude_skills, + use_cursor_rules, + ) } - Some(Commands::ListAgents) => run_list_agents(use_claude_skills), + Some(Commands::ListAgents) => run_list_agents(use_claude_skills, use_cursor_rules), 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..45225ef 100644 --- a/src/cli/tests.rs +++ b/src/cli/tests.rs @@ -12,6 +12,7 @@ fn test_generate_args_with_config_cli_priority() { no_gitignore: None, nested_depth: Some(5), use_claude_skills: None, + use_cursor_rules: None, }; let args = GenerateArgs { @@ -37,6 +38,7 @@ fn test_generate_args_with_config_uses_config_when_cli_missing() { no_gitignore: None, nested_depth: Some(3), use_claude_skills: None, + use_cursor_rules: None, }; let args = GenerateArgs { @@ -78,6 +80,7 @@ fn test_generate_args_with_config_partial_config() { no_gitignore: None, nested_depth: None, use_claude_skills: None, + use_cursor_rules: None, }; let args = GenerateArgs { @@ -103,6 +106,7 @@ fn test_nested_depth_args_with_config() { no_gitignore: None, nested_depth: Some(4), use_claude_skills: None, + use_cursor_rules: None, }; let args_with_cli = NestedDepthArgs { @@ -126,6 +130,7 @@ fn test_nested_depth_explicit_zero_overrides_config() { no_gitignore: None, nested_depth: Some(5), use_claude_skills: None, + use_cursor_rules: None, }; let args = NestedDepthArgs { @@ -144,6 +149,7 @@ fn test_status_args_with_config_cli_priority() { no_gitignore: None, nested_depth: Some(5), use_claude_skills: None, + use_cursor_rules: None, }; let args = StatusArgs { @@ -168,6 +174,7 @@ fn test_status_args_with_config_uses_config_when_cli_missing() { no_gitignore: None, nested_depth: Some(3), use_claude_skills: None, + use_cursor_rules: None, }; let args = StatusArgs { @@ -203,6 +210,7 @@ fn test_generate_args_backward_compat_no_gitignore_config() { no_gitignore: Some(true), nested_depth: None, use_claude_skills: None, + use_cursor_rules: None, }; let args = GenerateArgs { @@ -226,6 +234,7 @@ fn test_generate_args_backward_compat_no_gitignore_cli() { no_gitignore: None, nested_depth: None, use_claude_skills: None, + use_cursor_rules: None, }; let args = GenerateArgs { @@ -249,6 +258,7 @@ fn test_generate_args_new_gitignore_flag_overrides_old() { no_gitignore: None, nested_depth: None, use_claude_skills: None, + use_cursor_rules: None, }; let args = GenerateArgs { diff --git a/src/commands/clean.rs b/src/commands/clean.rs index 2c62ffe..eb8e903 100644 --- a/src/commands/clean.rs +++ b/src/commands/clean.rs @@ -4,9 +4,14 @@ 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, + use_claude_skills: bool, + use_cursor_rules: bool, +) -> Result<()> { println!("📋 Cleaning files for all agents, nested_depth: {nested_depth}"); - let registry = AgentToolRegistry::new(use_claude_skills); + let registry = AgentToolRegistry::new(use_claude_skills, use_cursor_rules); let agents: Vec = registry.get_all_tool_names(); let filter = file_utils::DirectoryFilter::from_project_root(current_dir); @@ -46,7 +51,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, false, false); assert!(result.is_ok()); assert_file_not_exists(project_path, "CLAUDE.md"); @@ -57,6 +62,25 @@ mod tests { assert_file_exists(project_path, "src/main.ts"); } + #[test] + fn test_run_clean_legacy_cursor_mode_removes_agents_md_and_cursor_rules() { + let temp_dir = TempDir::new().unwrap(); + let project_path = temp_dir.path(); + + create_file( + project_path, + ".cursor/rules/ai-rules-generated-test.mdc", + "Cursor rules", + ); + create_file(project_path, AGENTS_MD_FILENAME, "Generated agents"); + + let result = run_clean(project_path, CLEAN_NESTED_DEPTH, false, true); + assert!(result.is_ok()); + + assert_file_not_exists(project_path, ".cursor/rules/ai-rules-generated-test.mdc"); + assert_file_not_exists(project_path, AGENTS_MD_FILENAME); + } + #[test] fn test_run_clean_nested_folders() { let temp_dir = TempDir::new().unwrap(); @@ -87,7 +111,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, false, false); assert!(result.is_ok()); assert_file_not_exists(project_path, "subproject1/CLAUDE.md"); @@ -119,7 +143,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, false, false); assert!(result.is_ok()); assert_file_not_exists(project_path, "CLAUDE.md"); @@ -164,6 +188,7 @@ Test rule content"#; nested_depth: 2, }, false, + false, ); assert!(generate_result.is_ok()); @@ -171,7 +196,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, false, false); assert!(clean_result.is_ok()); assert_file_not_exists(project_path, "CLAUDE.md"); @@ -214,9 +239,16 @@ Test rule content"#; nested_depth: CLEAN_NESTED_DEPTH, }, false, + false, ); assert!(generate_result.is_ok()); + create_file( + project_path, + ".cursor/rules/ai-rules-generated-test.mdc", + "stale cursor rules", + ); + let expected_files = [ "CLAUDE.md", ".cursor/rules/ai-rules-generated-test.mdc", @@ -229,7 +261,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, false, false); assert!(clean_result.is_ok()); for file in &expected_files { @@ -272,7 +304,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, false, false); assert!(clean_result.is_ok()); // Legacy directories should be cleaned up @@ -295,7 +327,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, false, false); assert!(clean_result.is_ok()); // Generated file should be removed by legacy cleaner @@ -330,6 +362,7 @@ Test rule content"#; nested_depth: CLEAN_NESTED_DEPTH, }, false, + false, ); assert!(generate_result.is_ok()); @@ -342,7 +375,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, false, false); assert!(clean_result.is_ok()); // Verify skill symlink was removed @@ -377,6 +410,7 @@ Test rule content"#; nested_depth: CLEAN_NESTED_DEPTH, }, false, + false, ); assert!(generate_result.is_ok()); @@ -386,7 +420,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, false, false); assert!(clean_result.is_ok()); // Generated symlink should be removed diff --git a/src/commands/generate.rs b/src/commands/generate.rs index bded160..795b918 100644 --- a/src/commands/generate.rs +++ b/src/commands/generate.rs @@ -14,6 +14,7 @@ pub fn run_generate( current_dir: &Path, args: ResolvedGenerateArgs, use_claude_skills: bool, + use_cursor_rules: bool, ) -> Result<()> { println!( "Generating rules for agents: {}, nested_depth: {}, gitignore: {}", @@ -24,7 +25,7 @@ pub fn run_generate( args.nested_depth, args.gitignore ); - let registry = AgentToolRegistry::new(use_claude_skills); + let registry = AgentToolRegistry::new(use_claude_skills, use_cursor_rules); 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 +173,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, false, false); assert!(result.is_ok()); assert_file_exists(temp_dir.path(), ".gitignore"); @@ -188,7 +189,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, false, false); assert!(result.is_ok()); assert_file_exists( @@ -201,7 +202,7 @@ Test rule content"#; ); assert_file_exists(temp_dir.path(), "CLAUDE.md"); - assert_file_exists(temp_dir.path(), ".cursor/rules/ai-rules-generated-test.mdc"); + assert_file_not_exists(temp_dir.path(), ".cursor/rules/ai-rules-generated-test.mdc"); assert_file_exists(temp_dir.path(), AGENTS_MD_FILENAME); assert_file_exists(temp_dir.path(), ".gitignore"); @@ -218,18 +219,6 @@ Test rule content"#; let agents_content = std::fs::read_to_string(&agents_path).unwrap(); assert_eq!(agents_content, "# Test rule\n\nTest rule content\n"); - assert_file_content( - temp_dir.path(), - ".cursor/rules/ai-rules-generated-test.mdc", - r#"--- -description: Test rule -globs: **/*.ts -alwaysApply: true ---- - -Test rule content -"#, - ); assert_file_content( temp_dir.path(), "ai-rules/.generated-ai-rules/ai-rules-generated-test.md", @@ -239,10 +228,6 @@ Test rule content // Verify generated files have trailing newlines assert_file_has_trailing_newline(temp_dir.path(), "CLAUDE.md"); assert_file_has_trailing_newline(temp_dir.path(), AGENTS_MD_FILENAME); - assert_file_has_trailing_newline( - temp_dir.path(), - ".cursor/rules/ai-rules-generated-test.mdc", - ); assert_file_has_trailing_newline( temp_dir.path(), "ai-rules/.generated-ai-rules/ai-rules-generated-test.md", @@ -261,7 +246,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, false, false); assert!(result.is_ok()); assert_file_exists( @@ -284,7 +269,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, false, false); assert!(result.is_ok()); assert_file_exists( @@ -293,8 +278,8 @@ Test rule content ); assert_file_exists(temp_dir.path(), "CLAUDE.md"); - assert_file_exists(temp_dir.path(), ".cursor/rules/ai-rules-generated-test.mdc"); - assert_file_not_exists(temp_dir.path(), AGENTS_MD_FILENAME); + assert_file_not_exists(temp_dir.path(), ".cursor/rules/ai-rules-generated-test.mdc"); + assert_file_exists(temp_dir.path(), AGENTS_MD_FILENAME); assert_file_exists(temp_dir.path(), ".gitignore"); @@ -303,6 +288,26 @@ Test rule content assert!(claude_path.is_symlink(), "CLAUDE.md should be a symlink"); let claude_content = std::fs::read_to_string(&claude_path).unwrap(); assert_eq!(claude_content, "# Test rule\n\nTest rule content\n"); + let agents_path = temp_dir.path().join(AGENTS_MD_FILENAME); + assert!(agents_path.is_symlink(), "AGENTS.md should be a symlink"); + let agents_content = std::fs::read_to_string(&agents_path).unwrap(); + assert_eq!(agents_content, "# Test rule\n\nTest rule content\n"); + } + + #[test] + fn test_run_generate_cursor_legacy_opt_out() { + let temp_dir = TempDir::new().unwrap(); + + create_file(temp_dir.path(), "ai-rules/test.md", TEST_RULE_CONTENT); + + let args = ResolvedGenerateArgs { + agents: Some(vec!["cursor".to_string()]), + command_agents: None, + gitignore: false, + nested_depth: NESTED_DEPTH, + }; + let result = run_generate(temp_dir.path(), args, false, true); + assert!(result.is_ok()); assert_file_content( temp_dir.path(), @@ -316,6 +321,7 @@ alwaysApply: true Test rule content "#, ); + assert_file_not_exists(temp_dir.path(), AGENTS_MD_FILENAME); } #[test] @@ -333,7 +339,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, false, false); assert!(result.is_ok()); assert_file_exists( @@ -341,7 +347,8 @@ Test rule content "project1/ai-rules/.generated-ai-rules/ai-rules-generated-rule1.md", ); assert_file_exists(temp_dir.path(), "project1/CLAUDE.md"); - assert_file_exists( + assert_file_exists(temp_dir.path(), "project1/AGENTS.md"); + assert_file_not_exists( temp_dir.path(), "project1/.cursor/rules/ai-rules-generated-rule1.mdc", ); @@ -351,7 +358,8 @@ Test rule content "project1/nested/project2/ai-rules/.generated-ai-rules/ai-rules-generated-rule2.md", ); assert_file_exists(temp_dir.path(), "project1/nested/project2/CLAUDE.md"); - assert_file_exists( + assert_file_exists(temp_dir.path(), "project1/nested/project2/AGENTS.md"); + assert_file_not_exists( temp_dir.path(), "project1/nested/project2/.cursor/rules/ai-rules-generated-rule2.mdc", ); @@ -365,13 +373,13 @@ 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, false, false); assert!(result.is_ok()); // Check that gitignore contains patterns with ** prefix for subdirectory matching let gitignore_content = std::fs::read_to_string(temp_dir.path().join(".gitignore")).unwrap(); - assert!(gitignore_content.contains("**/.cursor/rules/")); + assert!(!gitignore_content.contains("**/.cursor/rules/")); assert!(gitignore_content.contains("**/ai-rules/.generated-ai-rules")); assert!(gitignore_content.contains(&format!("**/{AGENTS_MD_FILENAME}"))); assert!(gitignore_content.contains("**/CLAUDE.md")); @@ -394,7 +402,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, false, false); assert!(result.is_ok()); assert_file_exists( @@ -436,7 +444,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, false, false); assert!(result.is_ok()); assert_file_exists(temp_dir.path(), "CLAUDE.md"); @@ -460,7 +468,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(false, false); create_file( temp_dir.path(), @@ -504,7 +512,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(false, false); // First create normal files create_file(temp_dir.path(), "CLAUDE.md", "@.generated-ai-rules/old.md"); @@ -539,7 +547,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(false, false); create_file(temp_dir.path(), "ai-rules/AGENTS.md", "# Pure content"); let agents = vec!["claude".to_string(), "goose".to_string()]; @@ -570,7 +578,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(false, false); create_file(temp_dir.path(), "ai-rules/test.md", TEST_RULE_CONTENT); let agents = vec!["claude".to_string(), "cursor".to_string()]; @@ -594,18 +602,13 @@ Test rule content let cursor_files = &generation_result.files_by_agent["cursor"]; assert_eq!(claude_files[0], temp_dir.path().join("CLAUDE.md")); - assert_eq!( - cursor_files[0], - temp_dir - .path() - .join(".cursor/rules/ai-rules-generated-test.mdc") - ); + assert_eq!(cursor_files[0], temp_dir.path().join(AGENTS_MD_FILENAME)); } #[test] fn test_generate_files_normal_mode_cleans_symlinks() { let temp_dir = TempDir::new().unwrap(); - let registry = AgentToolRegistry::new(false); + let registry = AgentToolRegistry::new(false, false); create_file(temp_dir.path(), "ai-rules/AGENTS.md", "# Pure content"); let agents = vec!["claude".to_string()]; @@ -676,7 +679,7 @@ Optional content"#, gitignore: false, nested_depth: NESTED_DEPTH, }; - run_generate(temp_dir.path(), args.clone(), true).unwrap(); + run_generate(temp_dir.path(), args.clone(), true, false).unwrap(); assert_file_exists( temp_dir.path(), @@ -686,7 +689,7 @@ Optional content"#, 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(); + run_generate(temp_dir.path(), args, false, false).unwrap(); assert_file_not_exists(temp_dir.path(), ".claude/skills/"); assert_file_exists(temp_dir.path(), "ai-rules/.generated-ai-rules"); @@ -718,11 +721,11 @@ 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, false, false); assert!(result.is_ok()); assert_file_exists(temp_dir.path(), "CLAUDE.md"); - assert_file_exists(temp_dir.path(), ".cursor/rules/ai-rules-generated-test.mdc"); + assert_file_not_exists(temp_dir.path(), ".cursor/rules/ai-rules-generated-test.mdc"); assert_file_exists(temp_dir.path(), AGENTS_MD_FILENAME); // Roo now uses AGENTS.md assert_file_exists(temp_dir.path(), ".mcp.json"); @@ -754,12 +757,12 @@ 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, false, false); assert!(result.is_ok()); // Agent files should be created assert_file_exists(temp_dir.path(), "CLAUDE.md"); - assert_file_exists(temp_dir.path(), ".cursor/rules/ai-rules-generated-test.mdc"); + assert_file_exists(temp_dir.path(), AGENTS_MD_FILENAME); // MCP files should NOT be created assert_file_not_exists(temp_dir.path(), ".mcp.json"); @@ -779,7 +782,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, false, false); assert!(result.is_ok()); assert_file_exists(temp_dir.path(), "firebender.json"); @@ -806,7 +809,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, false, false); assert!(result.is_ok()); // Rule files: only AMP (AGENTS.md), no CLAUDE.md @@ -845,7 +848,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, false, false); assert!(result.is_ok()); // Both rules and commands for claude only @@ -881,7 +884,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, false, false); assert!(result.is_ok()); // Verify skill symlink was created @@ -915,7 +918,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, false, false); assert!(result.is_ok()); // Verify skill symlink was created in .agents/skills/ @@ -939,7 +942,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, false, false); 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..a144e91 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(use_claude_skills: bool, use_cursor_rules: bool) -> anyhow::Result<()> { + let registry = AgentToolRegistry::new(use_claude_skills, use_cursor_rules); 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 e2bdd54..52f74e5 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, false, false); if let Err(e) = &generate_result { panic!("Generate failed with error: {e}"); } @@ -81,8 +81,7 @@ mod tests { let generated_rule_path = "ai-rules/.generated-ai-rules/ai-rules-generated-example.md"; assert_file_exists(project_path, generated_rule_path); assert_file_exists(project_path, "CLAUDE.md"); - let cursor_rule_path = ".cursor/rules/ai-rules-generated-example.mdc"; - assert_file_exists(project_path, cursor_rule_path); + assert_file_not_exists(project_path, ".cursor/rules/ai-rules-generated-example.mdc"); assert_file_exists(project_path, AGENTS_MD_FILENAME); assert_file_exists(project_path, ".gitignore"); @@ -92,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, false, false).unwrap(); assert!(status_result.has_ai_rules); assert!(!status_result.body_files_out_of_sync); for in_sync in status_result.agent_statuses.values() { @@ -109,7 +108,8 @@ 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, false, false).unwrap(); assert!(status_after_change.has_ai_rules); assert!(!status_after_change.body_files_out_of_sync); @@ -127,7 +127,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, false, false); assert!(clean_result.is_ok()); assert_file_not_exists(project_path, "ai-rules/.generated-ai-rules"); @@ -154,7 +154,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, false, false); assert!(generate_result.is_ok()); // Verify all agents created symlinks pointing to the correct target @@ -179,7 +179,8 @@ 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, false, false).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..b30a287 100644 --- a/src/commands/status.rs +++ b/src/commands/status.rs @@ -31,6 +31,7 @@ pub fn run_status( current_dir: &Path, args: ResolvedStatusArgs, use_claude_skills: bool, + use_cursor_rules: bool, ) -> Result<()> { println!( "🔍 AI Rules Status for agents: {}, nested_depth: {}", @@ -41,7 +42,7 @@ 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, use_claude_skills, use_cursor_rules)?; print_status_results(&status); Ok(()) @@ -51,8 +52,9 @@ pub fn check_project_status( current_dir: &Path, args: ResolvedStatusArgs, use_claude_skills: bool, + use_cursor_rules: bool, ) -> Result { - let registry = AgentToolRegistry::new(use_claude_skills); + let registry = AgentToolRegistry::new(use_claude_skills, use_cursor_rules); 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 +289,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, false, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -307,7 +309,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, false, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -328,7 +330,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, false, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -355,7 +357,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, false, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -378,6 +380,7 @@ Test rule content"#; nested_depth: NESTED_DEPTH, }, false, + false, ) .unwrap(); @@ -394,7 +397,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, false, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -430,7 +433,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, false, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -463,6 +466,7 @@ Test rule content"#; nested_depth, }, false, + false, ) .unwrap(); @@ -471,7 +475,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, false, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -484,7 +488,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, false, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -509,6 +513,7 @@ Test rule content"#; nested_depth: 1, }, false, + false, ) .unwrap(); @@ -517,7 +522,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, false, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -544,6 +549,7 @@ Test rule content"#; nested_depth: NESTED_DEPTH, }, false, + false, ) .unwrap(); @@ -552,7 +558,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, false, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -588,6 +594,7 @@ Test rule content"#; nested_depth: NESTED_DEPTH, }, false, + false, ) .unwrap(); } @@ -609,7 +616,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, false, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -636,7 +643,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, false, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -662,6 +669,7 @@ Test rule content"#; nested_depth: NESTED_DEPTH, }, false, + false, ); assert!(generate_result.is_ok()); @@ -670,7 +678,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, false, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -704,6 +712,7 @@ Test command body"#; nested_depth: NESTED_DEPTH, }, false, + false, ) .unwrap(); } @@ -731,7 +740,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, false, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -764,6 +773,7 @@ Test command body"#; nested_depth: NESTED_DEPTH, }, false, + false, ) .unwrap(); @@ -778,7 +788,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, false, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -811,6 +821,7 @@ Test command body"#; nested_depth: NESTED_DEPTH, }, false, + false, ); assert!(generate_result.is_ok()); @@ -828,7 +839,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, false, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -865,6 +876,7 @@ Test command body"#; nested_depth: NESTED_DEPTH, }, false, + false, ); assert!(generate_result.is_ok()); @@ -873,7 +885,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, false, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -906,6 +918,7 @@ Test command body"#; nested_depth: NESTED_DEPTH, }, false, + false, ) .unwrap(); @@ -924,7 +937,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, false, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -955,6 +968,7 @@ Test command body"#; nested_depth: NESTED_DEPTH, }, false, + false, ); assert!(generate_result.is_ok()); @@ -963,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, false, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -990,6 +1004,7 @@ Test command body"#; nested_depth: NESTED_DEPTH, }, false, + false, ); assert!(generate_result.is_ok()); @@ -998,7 +1013,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, false, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -1030,6 +1045,7 @@ Test command body"#; nested_depth: NESTED_DEPTH, }, false, + false, ); assert!(generate_result.is_ok()); @@ -1041,7 +1057,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, false, false); assert!(result.is_ok()); let status = result.unwrap(); diff --git a/src/config.rs b/src/config.rs index bc02a1c..0be15ea 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,6 +12,8 @@ pub struct Config { pub no_gitignore: Option, pub nested_depth: Option, pub use_claude_skills: Option, + #[serde(alias = "useCursorRules", alias = "use_cursor_rule")] + pub use_cursor_rules: Option, } pub fn load_config(current_dir: &Path) -> Result> { @@ -174,6 +176,70 @@ agents: ["claude"] assert!(config.use_claude_skills.is_none()); } + #[test] + fn test_load_config_with_use_cursor_rules() { + let temp_dir = TempDir::new().unwrap(); + let config_content = r#" +agents: ["cursor"] +use_cursor_rules: 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!["cursor".to_string()])); + assert_eq!(config.use_cursor_rules, Some(true)); + } + + #[test] + fn test_load_config_with_use_cursor_rules_camel_case_alias() { + let temp_dir = TempDir::new().unwrap(); + let config_content = r#" +agents: ["cursor"] +useCursorRules: 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.use_cursor_rules, Some(true)); + } + + #[test] + fn test_load_config_with_use_cursor_rules_singular_alias() { + let temp_dir = TempDir::new().unwrap(); + let config_content = r#" +agents: ["cursor"] +use_cursor_rule: 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.use_cursor_rules, Some(true)); + } + + #[test] + fn test_load_config_without_use_cursor_rules_defaults_to_none() { + let temp_dir = TempDir::new().unwrap(); + let config_content = r#" +agents: ["cursor"] +"#; + 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_cursor_rules.is_none()); + } + #[test] fn test_load_config_backward_compatibility_no_gitignore() { let temp_dir = TempDir::new().unwrap(); diff --git a/src/operations/gitignore_updater.rs b/src/operations/gitignore_updater.rs index 037f883..217ce0d 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(false, false); 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(false, false); 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(false, false); let patterns = collect_all_gitignore_patterns(®istry, 1); // Check that skill patterns are included for agents that support skills From 58ea5c66b7f4af039b4af64a633a92b5d9fe1201 Mon Sep 17 00:00:00 2001 From: Jon Andersen Date: Wed, 25 Mar 2026 12:44:17 -0400 Subject: [PATCH 2/4] Remove Cursor legacy rules mode Drop the temporary Cursor opt-out and always generate AGENTS.md so Cursor behavior stays consistent across generate, status, and clean. Keep cleanup for stale .cursor/rules directories while simplifying the docs and examples. Signed-off-by: Jon Andersen Made-with: Cursor --- Example/ai-rules/ai-rules-config.yaml | 1 - README.md | 4 +- docs/agents.md | 2 +- docs/configuration.md | 12 +- docs/rule-format.md | 4 +- src/agents/cursor.rs | 314 +++++--------------------- src/agents/registry.rs | 4 +- src/cli/mod.rs | 27 +-- src/cli/tests.rs | 10 - src/commands/clean.rs | 50 +--- src/commands/generate.rs | 79 ++----- src/commands/list_agents.rs | 4 +- src/commands/mod.rs | 14 +- src/commands/status.rs | 62 ++--- src/config.rs | 66 ------ src/operations/gitignore_updater.rs | 6 +- 16 files changed, 134 insertions(+), 525 deletions(-) diff --git a/Example/ai-rules/ai-rules-config.yaml b/Example/ai-rules/ai-rules-config.yaml index f2dc489..7edcec5 100644 --- a/Example/ai-rules/ai-rules-config.yaml +++ b/Example/ai-rules/ai-rules-config.yaml @@ -1,4 +1,3 @@ agents: ["claude", "cursor", "firebender", "goose", "amp", "cline", "roo", "kilocode"] gitignore: true nested_depth: 2 -use_cursor_rules: false diff --git a/README.md b/README.md index 6a3e502..af4da33 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ curl -fsSL https://raw.githubusercontent.com/block/ai-rules/main/scripts/install ai-rules generate # All agents ai-rules generate --agents claude,cursor # Specific agents ``` - Creates `CLAUDE.md`, `AGENTS.md`, and other agent-specific files. Cursor uses `AGENTS.md` by default; set `use_cursor_rules: true` to keep the legacy `.cursor/rules/*.mdc` output. + Creates `CLAUDE.md`, `AGENTS.md`, and other agent-specific files. Cursor uses `AGENTS.md`. 4. **Check status** to ensure everything is in sync: ```bash @@ -79,8 +79,6 @@ Create `ai-rules/ai-rules-config.yaml` to set defaults: agents: [claude, cursor, cline] nested_depth: 2 gitignore: true -# Optional: keep legacy Cursor `.cursor/rules/*.mdc` output instead of `AGENTS.md` -# use_cursor_rules: true ``` See [Configuration Guide](docs/configuration.md) for all options. diff --git a/docs/agents.md b/docs/agents.md index 8224ecb..92ce885 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -9,7 +9,7 @@ | **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` | - | | -| **Cursor** | `AGENTS.md` -> inlined file | `AGENTS.md` -> `ai-rules/AGENTS.md` | `.cursor/mcp.json` | Set `use_cursor_rules: true` to keep legacy `.cursor/rules/*.mdc` output | +| **Cursor** | `AGENTS.md` -> inlined file | `AGENTS.md` -> `ai-rules/AGENTS.md` | `.cursor/mcp.json` | | | **Firebender** | `firebender.json` | `firebender.json` (references `ai-rules/AGENTS.md`) | Embedded in `firebender.json` | Supports overlay files | | **Gemini** | `GEMINI.md` -> inlined file | `GEMINI.md` -> `ai-rules/AGENTS.md` | Embedded in `.gemini/settings.json` | | | **Goose** | `AGENTS.md` -> inlined file | `AGENTS.md` -> `ai-rules/AGENTS.md` | - | | diff --git a/docs/configuration.md b/docs/configuration.md index 55131ca..b63f5fb 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -11,8 +11,6 @@ agents: [claude, cursor, cline] # Generate rules only for these agents command_agents: [claude, amp] # Generate commands for these agents (defaults to agents list if not specified) nested_depth: 2 # Search 2 levels deep for ai-rules/ folders gitignore: true # Ignore the generated rules in git -# Optional: keep legacy Cursor `.cursor/rules/*.mdc` output instead of `AGENTS.md` -# use_cursor_rules: true ``` ## Configuration Precedence @@ -35,12 +33,4 @@ Experimental toggle to test Claude Code's skills feature. When enabled, rules wi See [Commands and Skills](commands-and-skills.md) for more details on skills. -### Cursor Legacy Rules Mode - -```yaml -use_cursor_rules: true # Default: false -``` - -Cursor now uses `AGENTS.md` by default in standard mode so it shares the same generated content path as other `AGENTS.md`-based agents. Enable `use_cursor_rules` to opt out and keep the legacy `.cursor/rules/*.mdc` output. - -This opt-out is useful if you still rely on Cursor-specific `.mdc` metadata such as `fileMatching`/`globs`. +Cursor uses `AGENTS.md` in standard mode so it shares the same generated content path as other `AGENTS.md`-based agents. diff --git a/docs/rule-format.md b/docs/rule-format.md index 6a8b4c6..d64a982 100644 --- a/docs/rule-format.md +++ b/docs/rule-format.md @@ -28,7 +28,7 @@ All fields are optional: |-------|-------------|---------| | `description` | Context description that helps agents understand when to apply this rule if `alwaysApply` is `false` | - | | `alwaysApply` | `true` = referenced directly in agent rule files; `false` = included as optional rules based on context | `true` | -| `fileMatching` | Glob patterns for which files this rule applies to (e.g., `"**/*.ts"`, `"src/**/*.py"`). Currently supported when Cursor is using legacy `.cursor/rules/*.mdc` generation (`use_cursor_rules: true`). | - | +| `fileMatching` | Glob patterns for which files this rule applies to (e.g., `"**/*.ts"`, `"src/**/*.py"`). Currently used for conditional rule matching in Firebender. | - | If frontmatter is omitted entirely, the file is treated as a regular markdown rule with default settings (`alwaysApply: true`). @@ -38,7 +38,7 @@ In Standard Mode, `ai-rules generate` produces a single inlined file at `ai-rule Most agent output files (e.g., `CLAUDE.md`, `AGENTS.md`, `GEMINI.md`) are created as **symlinks** pointing to this inlined file. This ensures every agent reads the same complete content without needing `@` file expansion support. -Firebender generates its own JSON format and does not use the inlined file. Cursor uses the inlined `AGENTS.md` path by default, but can still generate legacy `.mdc` files when `use_cursor_rules: true`. +Firebender generates its own JSON format and does not use the inlined file. Cursor uses the inlined `AGENTS.md` path. ## Symlink Mode diff --git a/src/agents/cursor.rs b/src/agents/cursor.rs index 24a3425..7e310a9 100644 --- a/src/agents/cursor.rs +++ b/src/agents/cursor.rs @@ -6,35 +6,29 @@ use crate::agents::rule_generator::AgentRuleGenerator; use crate::agents::single_file_based::SingleFileBasedGenerator; use crate::agents::skills_generator::SkillsGeneratorTrait; use crate::constants::{ - AGENTS_MD_FILENAME, CURSOR_COMMANDS_DIR, CURSOR_COMMANDS_SUBDIR, CURSOR_SKILLS_DIR, - GENERATED_FILE_PREFIX, MCP_JSON, + AGENTS_MD_FILENAME, CURSOR_COMMANDS_DIR, CURSOR_COMMANDS_SUBDIR, CURSOR_SKILLS_DIR, MCP_JSON, }; use crate::models::SourceFile; -use crate::utils::file_utils::{check_directory_exact_match, ensure_trailing_newline}; use anyhow::Result; use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; -const MDC_EXTENSION: &str = "mdc"; - pub struct CursorGenerator { - use_cursor_rules: bool, - single_file: SingleFileBasedGenerator, + inner: SingleFileBasedGenerator, } impl CursorGenerator { - pub fn new(use_cursor_rules: bool) -> Self { + pub fn new() -> Self { Self { - use_cursor_rules, - single_file: SingleFileBasedGenerator::new("cursor", AGENTS_MD_FILENAME), + inner: SingleFileBasedGenerator::new("cursor", AGENTS_MD_FILENAME), } } } impl Default for CursorGenerator { fn default() -> Self { - Self::new(false) + Self::new() } } @@ -50,45 +44,6 @@ fn clean_cursor_rules_dir(current_dir: &Path) -> Result<()> { Ok(()) } -fn generate_cursor_rule_contents( - source_files: &[SourceFile], - current_dir: &Path, -) -> HashMap { - let mut agent_files = HashMap::new(); - - if source_files.is_empty() { - return agent_files; - } - - let cursor_rules_dir = get_cursor_rules_dir(current_dir); - - for source_file in source_files { - let generated_file_name = format!( - "{}{}.{}", - GENERATED_FILE_PREFIX, source_file.base_file_name, MDC_EXTENSION - ); - - let cursor_file_path = cursor_rules_dir.join(generated_file_name); - - if let Ok(content) = generate_rule_file_content(source_file) { - agent_files.insert(cursor_file_path, content); - } - } - - agent_files -} - -fn check_cursor_rule_contents(source_files: &[SourceFile], current_dir: &Path) -> Result { - let cursor_rules_dir = get_cursor_rules_dir(current_dir); - - if source_files.is_empty() { - return Ok(!cursor_rules_dir.exists()); - } - - let expected_files = generate_cursor_rule_contents(source_files, current_dir); - check_directory_exact_match(&cursor_rules_dir, &expected_files) -} - impl AgentRuleGenerator for CursorGenerator { fn name(&self) -> &str { "cursor" @@ -96,10 +51,7 @@ impl AgentRuleGenerator for CursorGenerator { fn clean(&self, current_dir: &Path) -> Result<()> { clean_cursor_rules_dir(current_dir)?; - if !self.use_cursor_rules { - self.single_file.clean(current_dir)?; - } - Ok(()) + self.inner.clean(current_dir) } fn generate_agent_contents( @@ -107,12 +59,8 @@ impl AgentRuleGenerator for CursorGenerator { source_files: &[SourceFile], current_dir: &Path, ) -> HashMap { - if self.use_cursor_rules { - generate_cursor_rule_contents(source_files, current_dir) - } else { - self.single_file - .generate_agent_contents(source_files, current_dir) - } + self.inner + .generate_agent_contents(source_files, current_dir) } fn check_agent_contents( @@ -120,45 +68,34 @@ impl AgentRuleGenerator for CursorGenerator { source_files: &[SourceFile], current_dir: &Path, ) -> Result { - if self.use_cursor_rules { - check_cursor_rule_contents(source_files, current_dir) - } else { - Ok(self - .single_file - .check_agent_contents(source_files, current_dir)? - && !get_cursor_rules_dir(current_dir).exists()) - } + Ok(self.inner.check_agent_contents(source_files, current_dir)? + && !get_cursor_rules_dir(current_dir).exists()) } fn check_symlink(&self, current_dir: &Path) -> Result { - Ok(self.single_file.check_symlink(current_dir)? - && !get_cursor_rules_dir(current_dir).exists()) + Ok(self.inner.check_symlink(current_dir)? && !get_cursor_rules_dir(current_dir).exists()) } fn gitignore_patterns(&self) -> Vec { - if self.use_cursor_rules { - vec![".cursor/rules/".to_string()] - } else { - self.single_file.gitignore_patterns() - } + self.inner.gitignore_patterns() } fn generate_symlink(&self, current_dir: &Path) -> Result> { clean_cursor_rules_dir(current_dir)?; - self.single_file.generate_symlink(current_dir) + self.inner.generate_symlink(current_dir) } fn uses_inlined_symlink(&self) -> bool { - !self.use_cursor_rules + true } fn generate_inlined_symlink(&self, current_dir: &Path) -> Result> { clean_cursor_rules_dir(current_dir)?; - self.single_file.generate_inlined_symlink(current_dir) + self.inner.generate_inlined_symlink(current_dir) } fn check_inlined_symlink(&self, current_dir: &Path) -> Result { - Ok(self.single_file.check_inlined_symlink(current_dir)? + Ok(self.inner.check_inlined_symlink(current_dir)? && !get_cursor_rules_dir(current_dir).exists()) } @@ -180,40 +117,12 @@ impl AgentRuleGenerator for CursorGenerator { } } -fn create_cursor_frontmatter(source_file: &SourceFile) -> String { - let globs_section = match &source_file.front_matter.file_matching_patterns { - Some(patterns) if !patterns.is_empty() => format!("globs: {}\n", patterns.join(", ")), - _ => String::new(), - }; - - format!( - "---\ndescription: {}\n{}alwaysApply: {}\n---\n\n", - source_file.front_matter.description, globs_section, source_file.front_matter.always_apply - ) -} - -fn generate_rule_file_content(source_file: &SourceFile) -> Result { - let mut cursor_content = create_cursor_frontmatter(source_file); - cursor_content.push_str(&source_file.body); - - Ok(ensure_trailing_newline(cursor_content)) -} - #[cfg(test)] mod tests { use super::*; - use crate::{models::source_file::FrontMatter, utils::test_utils::helpers::*}; + use crate::utils::test_utils::helpers::*; use tempfile::TempDir; - const EXPECTED_TEST_RULE_CONTENT: &str = r#"--- -description: Test rule -globs: **/*.ts -alwaysApply: true ---- - -test body -"#; - fn create_standard_test_source_file() -> SourceFile { create_test_source_file( "test", @@ -225,55 +134,14 @@ test body } #[test] - fn test_create_cursor_frontmatter() { - let source_file = create_standard_test_source_file(); - - let frontmatter = create_cursor_frontmatter(&source_file); - let expected = r#"--- -description: Test rule -globs: **/*.ts -alwaysApply: true ---- - -"#; - - assert_eq!(frontmatter, expected); - } - - #[test] - fn test_create_cursor_frontmatter_file_matching_patterns_empty() { - let source_file = SourceFile { - base_file_name: "test".to_string(), - front_matter: FrontMatter { - description: "Test rule".to_string(), - always_apply: true, - file_matching_patterns: None, - }, - body: "test body".to_string(), - }; - - let frontmatter = create_cursor_frontmatter(&source_file); - let expected = r#"--- -description: Test rule -alwaysApply: true ---- - -"#; - - assert_eq!(frontmatter, expected); - } - - #[test] - fn test_generate_rule_file_content() { - let source_file = create_standard_test_source_file(); - - let content = generate_rule_file_content(&source_file).unwrap(); + fn test_cursor_generator_name() { + let generator = CursorGenerator::default(); - assert_eq!(content, EXPECTED_TEST_RULE_CONTENT); + assert_eq!(generator.name(), "cursor"); } #[test] - fn test_default_cursor_gitignore_patterns_use_agents_md() { + fn test_cursor_gitignore_patterns_use_agents_md() { let generator = CursorGenerator::default(); let patterns = generator.gitignore_patterns(); @@ -281,15 +149,7 @@ alwaysApply: true } #[test] - fn test_legacy_cursor_gitignore_patterns_use_cursor_rules_dir() { - let generator = CursorGenerator::new(true); - let patterns = generator.gitignore_patterns(); - - assert_eq!(patterns, vec![".cursor/rules/".to_string()]); - } - - #[test] - fn test_generate_agent_contents_default_uses_agents_md() { + fn test_generate_agent_contents_uses_agents_md() { let generator = CursorGenerator::default(); let temp_dir = TempDir::new().unwrap(); let source_files = vec![create_standard_test_source_file()]; @@ -305,61 +165,6 @@ alwaysApply: true ); } - #[test] - fn test_generate_agent_contents_legacy_cursor_rules_mode() { - let generator = CursorGenerator::new(true); - let temp_dir = TempDir::new().unwrap(); - let source_files = vec![ - create_test_source_file( - "rule1", - "First rule", - true, - vec!["**/*.ts".to_string()], - "rule1 body", - ), - create_test_source_file( - "rule2", - "Second rule", - false, - vec!["**/*.js".to_string()], - "rule2 body", - ), - ]; - - let result = generator.generate_agent_contents(&source_files, temp_dir.path()); - - assert_eq!(result.len(), 2); - - let expected_path1 = temp_dir - .path() - .join(".cursor/rules/ai-rules-generated-rule1.mdc"); - let expected_path2 = temp_dir - .path() - .join(".cursor/rules/ai-rules-generated-rule2.mdc"); - - let content1 = result.get(&expected_path1).unwrap(); - let expected_content1 = r#"--- -description: First rule -globs: **/*.ts -alwaysApply: true ---- - -rule1 body -"#; - assert_eq!(content1, &expected_content1); - - let content2 = result.get(&expected_path2).unwrap(); - let expected_content2 = r#"--- -description: Second rule -globs: **/*.js -alwaysApply: false ---- - -rule2 body -"#; - assert_eq!(content2, &expected_content2); - } - #[test] fn test_clean_non_existing_directory() { let generator = CursorGenerator::default(); @@ -372,7 +177,7 @@ rule2 body } #[test] - fn test_clean_default_removes_existing_directory() { + fn test_clean_removes_existing_directory() { let generator = CursorGenerator::default(); let temp_dir = TempDir::new().unwrap(); create_file( @@ -380,7 +185,6 @@ rule2 body ".cursor/settings.json", "existing settings content", ); - create_file( temp_dir.path(), ".cursor/rules/ai-rules-generated-test.mdc", @@ -401,7 +205,7 @@ rule2 body } #[test] - fn test_clean_default_removes_agents_md_symlink() { + fn test_clean_removes_agents_md_symlink() { let generator = CursorGenerator::default(); let temp_dir = TempDir::new().unwrap(); @@ -422,8 +226,8 @@ rule2 body } #[test] - fn test_clean_legacy_preserves_existing_agents_md() { - let generator = CursorGenerator::new(true); + fn test_clean_removes_stale_cursor_rules_dir_and_agents_md() { + let generator = CursorGenerator::default(); let temp_dir = TempDir::new().unwrap(); create_file(temp_dir.path(), AGENTS_MD_FILENAME, "shared agents content"); @@ -435,13 +239,13 @@ rule2 body generator.clean(temp_dir.path()).unwrap(); - assert_file_exists(temp_dir.path(), AGENTS_MD_FILENAME); + assert_file_not_exists(temp_dir.path(), AGENTS_MD_FILENAME); assert_file_not_exists(temp_dir.path(), ".cursor/rules"); } #[test] - fn test_check_empty_source_files_with_directory() { - let generator = CursorGenerator::new(true); + fn test_check_empty_source_files_with_stale_cursor_rules_dir() { + let generator = CursorGenerator::default(); let temp_dir = TempDir::new().unwrap(); create_file( @@ -458,47 +262,51 @@ rule2 body } #[test] - fn test_check_with_matching_files() { - let generator = CursorGenerator::new(true); + fn test_check_symlink_with_correct_symlink() { let temp_dir = TempDir::new().unwrap(); - let source_file = create_standard_test_source_file(); + let generator = CursorGenerator::default(); - create_file( - temp_dir.path(), - ".cursor/rules/ai-rules-generated-test.mdc", - EXPECTED_TEST_RULE_CONTENT, - ); + create_file(temp_dir.path(), "ai-rules/AGENTS.md", "# Source content"); - let result = generator - .check_agent_contents(&[source_file], temp_dir.path()) - .unwrap(); + let result = generator.generate_symlink(temp_dir.path()); + assert!(result.is_ok()); + let result = generator.check_symlink(temp_dir.path()).unwrap(); assert!(result); } #[test] - fn test_check_with_missing_files() { - let generator = CursorGenerator::new(true); + fn test_check_symlink_rejects_stale_cursor_rules_dir() { let temp_dir = TempDir::new().unwrap(); - let source_file = create_standard_test_source_file(); + let generator = CursorGenerator::default(); - let result = generator - .check_agent_contents(&[source_file], temp_dir.path()) - .unwrap(); + create_file(temp_dir.path(), "ai-rules/AGENTS.md", "# Source content"); + generator.generate_symlink(temp_dir.path()).unwrap(); + create_file( + temp_dir.path(), + ".cursor/rules/ai-rules-generated-stale.mdc", + "stale content", + ); + let result = generator.check_symlink(temp_dir.path()).unwrap(); assert!(!result); } #[test] - fn test_check_with_incorrect_content() { - let generator = CursorGenerator::new(true); + fn test_check_agent_contents_rejects_stale_cursor_rules_dir() { let temp_dir = TempDir::new().unwrap(); + let generator = CursorGenerator::default(); let source_file = create_standard_test_source_file(); create_file( temp_dir.path(), - ".cursor/rules/ai-rules-generated-test.mdc", - "wrong content", + AGENTS_MD_FILENAME, + "@ai-rules/.generated-ai-rules/ai-rules-generated-test.md\n", + ); + create_file( + temp_dir.path(), + ".cursor/rules/ai-rules-generated-stale.mdc", + "stale content", ); let result = generator @@ -508,20 +316,6 @@ rule2 body assert!(!result); } - #[test] - fn test_check_symlink_with_correct_symlink() { - let generator = CursorGenerator::default(); - let temp_dir = TempDir::new().unwrap(); - - create_file(temp_dir.path(), "ai-rules/AGENTS.md", "# Source content"); - - let result = generator.generate_symlink(temp_dir.path()); - assert!(result.is_ok()); - - let result = generator.check_symlink(temp_dir.path()).unwrap(); - assert!(result); - } - #[test] fn test_check_inlined_symlink_rejects_stale_cursor_rules_dir() { let generator = CursorGenerator::default(); diff --git a/src/agents/registry.rs b/src/agents/registry.rs index 6512ace..12d5565 100644 --- a/src/agents/registry.rs +++ b/src/agents/registry.rs @@ -12,7 +12,7 @@ pub struct AgentToolRegistry { } impl AgentToolRegistry { - pub fn new(use_claude_skills: bool, use_cursor_rules: bool) -> Self { + pub fn new(use_claude_skills: bool) -> Self { let mut tools: HashMap> = HashMap::new(); // Claude now always uses ClaudeGenerator with skills_mode parameter @@ -25,7 +25,7 @@ impl AgentToolRegistry { let generators: Vec> = vec![ claude_generator, Box::new(SingleFileBasedGenerator::new("cline", AGENTS_MD_FILENAME)), - Box::new(CursorGenerator::new(use_cursor_rules)), + Box::new(CursorGenerator::new()), Box::new(FirebenderGenerator), Box::new(SingleFileBasedGenerator::new("goose", AGENTS_MD_FILENAME)), Box::new(AmpGenerator), diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 9b359f9..5a708f9 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -28,41 +28,22 @@ pub fn run_cli() -> anyhow::Result<()> { .as_ref() .and_then(|c| c.use_claude_skills) .unwrap_or(false); - let use_cursor_rules = config - .as_ref() - .and_then(|c| c.use_cursor_rules) - .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, - use_cursor_rules, - ) + run_generate(¤t_dir, final_args, use_claude_skills) } Some(Commands::Status(args)) => { let final_args = args.with_config(config.as_ref()); - run_status( - ¤t_dir, - final_args, - use_claude_skills, - use_cursor_rules, - ) + run_status(¤t_dir, final_args, use_claude_skills) } 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, - use_cursor_rules, - ) + run_clean(¤t_dir, nested_depth, use_claude_skills) } - Some(Commands::ListAgents) => run_list_agents(use_claude_skills, use_cursor_rules), + Some(Commands::ListAgents) => run_list_agents(use_claude_skills), 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 45225ef..8a9615b 100644 --- a/src/cli/tests.rs +++ b/src/cli/tests.rs @@ -12,7 +12,6 @@ fn test_generate_args_with_config_cli_priority() { no_gitignore: None, nested_depth: Some(5), use_claude_skills: None, - use_cursor_rules: None, }; let args = GenerateArgs { @@ -38,7 +37,6 @@ fn test_generate_args_with_config_uses_config_when_cli_missing() { no_gitignore: None, nested_depth: Some(3), use_claude_skills: None, - use_cursor_rules: None, }; let args = GenerateArgs { @@ -80,7 +78,6 @@ fn test_generate_args_with_config_partial_config() { no_gitignore: None, nested_depth: None, use_claude_skills: None, - use_cursor_rules: None, }; let args = GenerateArgs { @@ -106,7 +103,6 @@ fn test_nested_depth_args_with_config() { no_gitignore: None, nested_depth: Some(4), use_claude_skills: None, - use_cursor_rules: None, }; let args_with_cli = NestedDepthArgs { @@ -130,7 +126,6 @@ fn test_nested_depth_explicit_zero_overrides_config() { no_gitignore: None, nested_depth: Some(5), use_claude_skills: None, - use_cursor_rules: None, }; let args = NestedDepthArgs { @@ -149,7 +144,6 @@ fn test_status_args_with_config_cli_priority() { no_gitignore: None, nested_depth: Some(5), use_claude_skills: None, - use_cursor_rules: None, }; let args = StatusArgs { @@ -174,7 +168,6 @@ fn test_status_args_with_config_uses_config_when_cli_missing() { no_gitignore: None, nested_depth: Some(3), use_claude_skills: None, - use_cursor_rules: None, }; let args = StatusArgs { @@ -210,7 +203,6 @@ fn test_generate_args_backward_compat_no_gitignore_config() { no_gitignore: Some(true), nested_depth: None, use_claude_skills: None, - use_cursor_rules: None, }; let args = GenerateArgs { @@ -234,7 +226,6 @@ fn test_generate_args_backward_compat_no_gitignore_cli() { no_gitignore: None, nested_depth: None, use_claude_skills: None, - use_cursor_rules: None, }; let args = GenerateArgs { @@ -258,7 +249,6 @@ fn test_generate_args_new_gitignore_flag_overrides_old() { no_gitignore: None, nested_depth: None, use_claude_skills: None, - use_cursor_rules: None, }; let args = GenerateArgs { diff --git a/src/commands/clean.rs b/src/commands/clean.rs index eb8e903..169d884 100644 --- a/src/commands/clean.rs +++ b/src/commands/clean.rs @@ -4,14 +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, - use_cursor_rules: bool, -) -> Result<()> { +pub fn run_clean(current_dir: &Path, nested_depth: usize, use_claude_skills: bool) -> Result<()> { println!("📋 Cleaning files for all agents, nested_depth: {nested_depth}"); - let registry = AgentToolRegistry::new(use_claude_skills, use_cursor_rules); + let registry = AgentToolRegistry::new(use_claude_skills); let agents: Vec = registry.get_all_tool_names(); let filter = file_utils::DirectoryFilter::from_project_root(current_dir); @@ -51,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, false); + let result = run_clean(project_path, CLEAN_NESTED_DEPTH, false); assert!(result.is_ok()); assert_file_not_exists(project_path, "CLAUDE.md"); @@ -62,25 +57,6 @@ mod tests { assert_file_exists(project_path, "src/main.ts"); } - #[test] - fn test_run_clean_legacy_cursor_mode_removes_agents_md_and_cursor_rules() { - let temp_dir = TempDir::new().unwrap(); - let project_path = temp_dir.path(); - - create_file( - project_path, - ".cursor/rules/ai-rules-generated-test.mdc", - "Cursor rules", - ); - create_file(project_path, AGENTS_MD_FILENAME, "Generated agents"); - - let result = run_clean(project_path, CLEAN_NESTED_DEPTH, false, true); - assert!(result.is_ok()); - - assert_file_not_exists(project_path, ".cursor/rules/ai-rules-generated-test.mdc"); - assert_file_not_exists(project_path, AGENTS_MD_FILENAME); - } - #[test] fn test_run_clean_nested_folders() { let temp_dir = TempDir::new().unwrap(); @@ -111,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, false); + let result = run_clean(project_path, CLEAN_NESTED_DEPTH, false); assert!(result.is_ok()); assert_file_not_exists(project_path, "subproject1/CLAUDE.md"); @@ -143,7 +119,7 @@ mod tests { create_file(project_path, "src/main.rs", "fn main() {}"); - let result = run_clean(project_path, CLEAN_NESTED_DEPTH, false, false); + let result = run_clean(project_path, CLEAN_NESTED_DEPTH, false); assert!(result.is_ok()); assert_file_not_exists(project_path, "CLAUDE.md"); @@ -188,7 +164,6 @@ Test rule content"#; nested_depth: 2, }, false, - false, ); assert!(generate_result.is_ok()); @@ -196,7 +171,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, false); + let clean_result = run_clean(project_path, 0, false); assert!(clean_result.is_ok()); assert_file_not_exists(project_path, "CLAUDE.md"); @@ -239,7 +214,6 @@ Test rule content"#; nested_depth: CLEAN_NESTED_DEPTH, }, false, - false, ); assert!(generate_result.is_ok()); @@ -261,7 +235,7 @@ Test rule content"#; assert_file_exists(project_path, file); } - let clean_result = run_clean(project_path, CLEAN_NESTED_DEPTH, false, false); + let clean_result = run_clean(project_path, CLEAN_NESTED_DEPTH, false); assert!(clean_result.is_ok()); for file in &expected_files { @@ -304,7 +278,7 @@ Test rule content"#; "old kilocode content", ); - let clean_result = run_clean(project_path, CLEAN_NESTED_DEPTH, false, false); + let clean_result = run_clean(project_path, CLEAN_NESTED_DEPTH, false); assert!(clean_result.is_ok()); // Legacy directories should be cleaned up @@ -327,7 +301,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, false); + let clean_result = run_clean(project_path, CLEAN_NESTED_DEPTH, false); assert!(clean_result.is_ok()); // Generated file should be removed by legacy cleaner @@ -362,7 +336,6 @@ Test rule content"#; nested_depth: CLEAN_NESTED_DEPTH, }, false, - false, ); assert!(generate_result.is_ok()); @@ -375,7 +348,7 @@ Test rule content"#; assert!(symlink_path.is_symlink()); // Clean - let clean_result = run_clean(project_path, CLEAN_NESTED_DEPTH, false, false); + let clean_result = run_clean(project_path, CLEAN_NESTED_DEPTH, false); assert!(clean_result.is_ok()); // Verify skill symlink was removed @@ -410,7 +383,6 @@ Test rule content"#; nested_depth: CLEAN_NESTED_DEPTH, }, false, - false, ); assert!(generate_result.is_ok()); @@ -420,7 +392,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, false); + let clean_result = run_clean(project_path, CLEAN_NESTED_DEPTH, false); assert!(clean_result.is_ok()); // Generated symlink should be removed diff --git a/src/commands/generate.rs b/src/commands/generate.rs index 795b918..073e3a2 100644 --- a/src/commands/generate.rs +++ b/src/commands/generate.rs @@ -14,7 +14,6 @@ pub fn run_generate( current_dir: &Path, args: ResolvedGenerateArgs, use_claude_skills: bool, - use_cursor_rules: bool, ) -> Result<()> { println!( "Generating rules for agents: {}, nested_depth: {}, gitignore: {}", @@ -25,7 +24,7 @@ pub fn run_generate( args.nested_depth, args.gitignore ); - let registry = AgentToolRegistry::new(use_claude_skills, use_cursor_rules); + let registry = AgentToolRegistry::new(use_claude_skills); let agents = args.agents.unwrap_or_else(|| registry.get_all_tool_names()); let command_agents = args.command_agents.unwrap_or_else(|| agents.clone()); @@ -173,7 +172,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, false); + let result = run_generate(temp_dir.path(), GENERATE_ARGS, false); assert!(result.is_ok()); assert_file_exists(temp_dir.path(), ".gitignore"); @@ -189,7 +188,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, false); + let result = run_generate(temp_dir.path(), GENERATE_ARGS, false); assert!(result.is_ok()); assert_file_exists( @@ -246,7 +245,7 @@ Test rule content"#; gitignore: false, nested_depth: NESTED_DEPTH, }; - let result = run_generate(temp_dir.path(), args, false, false); + let result = run_generate(temp_dir.path(), args, false); assert!(result.is_ok()); assert_file_exists( @@ -269,7 +268,7 @@ Test rule content"#; gitignore: true, nested_depth: NESTED_DEPTH, }; - let result = run_generate(temp_dir.path(), args, false, false); + let result = run_generate(temp_dir.path(), args, false); assert!(result.is_ok()); assert_file_exists( @@ -294,36 +293,6 @@ Test rule content"#; assert_eq!(agents_content, "# Test rule\n\nTest rule content\n"); } - #[test] - fn test_run_generate_cursor_legacy_opt_out() { - let temp_dir = TempDir::new().unwrap(); - - create_file(temp_dir.path(), "ai-rules/test.md", TEST_RULE_CONTENT); - - let args = ResolvedGenerateArgs { - agents: Some(vec!["cursor".to_string()]), - command_agents: None, - gitignore: false, - nested_depth: NESTED_DEPTH, - }; - let result = run_generate(temp_dir.path(), args, false, true); - assert!(result.is_ok()); - - assert_file_content( - temp_dir.path(), - ".cursor/rules/ai-rules-generated-test.mdc", - r#"--- -description: Test rule -globs: **/*.ts -alwaysApply: true ---- - -Test rule content -"#, - ); - assert_file_not_exists(temp_dir.path(), AGENTS_MD_FILENAME); - } - #[test] fn test_run_generate_nested_projects() { let temp_dir = TempDir::new().unwrap(); @@ -339,7 +308,7 @@ Test rule content TEST_RULE_CONTENT, ); - let result = run_generate(temp_dir.path(), GENERATE_ARGS, false, false); + let result = run_generate(temp_dir.path(), GENERATE_ARGS, false); assert!(result.is_ok()); assert_file_exists( @@ -373,7 +342,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, false); + let result = run_generate(temp_dir.path(), GENERATE_ARGS, false); assert!(result.is_ok()); // Check that gitignore contains patterns with ** prefix for subdirectory matching @@ -402,7 +371,7 @@ Test rule content gitignore: true, nested_depth: 0, }; - let result = run_generate(temp_dir.path(), args, false, false); + let result = run_generate(temp_dir.path(), args, false); assert!(result.is_ok()); assert_file_exists( @@ -444,7 +413,7 @@ Test rule content gitignore: false, nested_depth: NESTED_DEPTH, }; - let result = run_generate(temp_dir.path(), args, false, false); + let result = run_generate(temp_dir.path(), args, false); assert!(result.is_ok()); assert_file_exists(temp_dir.path(), "CLAUDE.md"); @@ -468,7 +437,7 @@ Test rule content #[test] fn test_generate_files_symlink_mode() { let temp_dir = TempDir::new().unwrap(); - let registry = AgentToolRegistry::new(false, false); + let registry = AgentToolRegistry::new(false); create_file( temp_dir.path(), @@ -512,7 +481,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, false); + let registry = AgentToolRegistry::new(false); // First create normal files create_file(temp_dir.path(), "CLAUDE.md", "@.generated-ai-rules/old.md"); @@ -547,7 +516,7 @@ Test rule content #[test] fn test_generation_result_agent_listing_symlink_mode() { let temp_dir = TempDir::new().unwrap(); - let registry = AgentToolRegistry::new(false, false); + let registry = AgentToolRegistry::new(false); create_file(temp_dir.path(), "ai-rules/AGENTS.md", "# Pure content"); let agents = vec!["claude".to_string(), "goose".to_string()]; @@ -578,7 +547,7 @@ Test rule content #[test] fn test_generation_result_agent_listing_normal_mode() { let temp_dir = TempDir::new().unwrap(); - let registry = AgentToolRegistry::new(false, false); + let registry = AgentToolRegistry::new(false); create_file(temp_dir.path(), "ai-rules/test.md", TEST_RULE_CONTENT); let agents = vec!["claude".to_string(), "cursor".to_string()]; @@ -608,7 +577,7 @@ Test rule content #[test] fn test_generate_files_normal_mode_cleans_symlinks() { let temp_dir = TempDir::new().unwrap(); - let registry = AgentToolRegistry::new(false, false); + let registry = AgentToolRegistry::new(false); create_file(temp_dir.path(), "ai-rules/AGENTS.md", "# Pure content"); let agents = vec!["claude".to_string()]; @@ -679,7 +648,7 @@ Optional content"#, gitignore: false, nested_depth: NESTED_DEPTH, }; - run_generate(temp_dir.path(), args.clone(), true, false).unwrap(); + run_generate(temp_dir.path(), args.clone(), true).unwrap(); assert_file_exists( temp_dir.path(), @@ -689,7 +658,7 @@ Optional content"#, 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, false).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"); @@ -721,7 +690,7 @@ Optional content"#, gitignore: false, nested_depth: NESTED_DEPTH, }; - let result = run_generate(temp_dir.path(), args, false, false); + let result = run_generate(temp_dir.path(), args, false); assert!(result.is_ok()); assert_file_exists(temp_dir.path(), "CLAUDE.md"); @@ -757,7 +726,7 @@ Optional content"#, gitignore: false, nested_depth: NESTED_DEPTH, }; - let result = run_generate(temp_dir.path(), args, false, false); + let result = run_generate(temp_dir.path(), args, false); assert!(result.is_ok()); // Agent files should be created @@ -782,7 +751,7 @@ Optional content"#, gitignore: false, nested_depth: NESTED_DEPTH, }; - let result = run_generate(temp_dir.path(), args, false, false); + let result = run_generate(temp_dir.path(), args, false); assert!(result.is_ok()); assert_file_exists(temp_dir.path(), "firebender.json"); @@ -809,7 +778,7 @@ Optional content"#, gitignore: false, nested_depth: NESTED_DEPTH, }; - let result = run_generate(temp_dir.path(), args, false, false); + let result = run_generate(temp_dir.path(), args, false); assert!(result.is_ok()); // Rule files: only AMP (AGENTS.md), no CLAUDE.md @@ -848,7 +817,7 @@ Optional content"#, gitignore: false, nested_depth: NESTED_DEPTH, }; - let result = run_generate(temp_dir.path(), args, false, false); + let result = run_generate(temp_dir.path(), args, false); assert!(result.is_ok()); // Both rules and commands for claude only @@ -884,7 +853,7 @@ Optional content"#, gitignore: false, nested_depth: NESTED_DEPTH, }; - let result = run_generate(temp_dir.path(), args, false, false); + let result = run_generate(temp_dir.path(), args, false); assert!(result.is_ok()); // Verify skill symlink was created @@ -918,7 +887,7 @@ Optional content"#, gitignore: false, nested_depth: NESTED_DEPTH, }; - let result = run_generate(temp_dir.path(), args, false, false); + let result = run_generate(temp_dir.path(), args, false); assert!(result.is_ok()); // Verify skill symlink was created in .agents/skills/ @@ -942,7 +911,7 @@ Optional content"#, gitignore: false, nested_depth: NESTED_DEPTH, }; - let result = run_generate(temp_dir.path(), args, false, false); + let result = run_generate(temp_dir.path(), args, false); 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 a144e91..2e8a87a 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, use_cursor_rules: bool) -> anyhow::Result<()> { - let registry = AgentToolRegistry::new(use_claude_skills, use_cursor_rules); +pub fn run_list_agents(use_claude_skills: bool) -> anyhow::Result<()> { + let registry = AgentToolRegistry::new(use_claude_skills); 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 52f74e5..9fc7818 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, false); + let generate_result = run_generate(project_path, generate_args, false); 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, false).unwrap(); + let status_result = check_project_status(project_path, status_args, false).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,8 +108,7 @@ mod tests { command_agents: None, nested_depth, }; - let status_after_change = - check_project_status(project_path, status_args, false, false).unwrap(); + let status_after_change = check_project_status(project_path, status_args, false).unwrap(); assert!(status_after_change.has_ai_rules); assert!(!status_after_change.body_files_out_of_sync); @@ -127,7 +126,7 @@ mod tests { ); // Clean - should remove all generated files - let clean_result = run_clean(project_path, nested_depth, false, false); + let clean_result = run_clean(project_path, nested_depth, false); assert!(clean_result.is_ok()); assert_file_not_exists(project_path, "ai-rules/.generated-ai-rules"); @@ -154,7 +153,7 @@ mod tests { gitignore: true, nested_depth, }; - let generate_result = run_generate(project_path, generate_args, false, false); + let generate_result = run_generate(project_path, generate_args, false); assert!(generate_result.is_ok()); // Verify all agents created symlinks pointing to the correct target @@ -179,8 +178,7 @@ mod tests { command_agents: None, nested_depth, }; - let status_after_change = - check_project_status(project_path, status_args, false, false).unwrap(); + let status_after_change = check_project_status(project_path, status_args, false).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 b30a287..a3f17c4 100644 --- a/src/commands/status.rs +++ b/src/commands/status.rs @@ -31,7 +31,6 @@ pub fn run_status( current_dir: &Path, args: ResolvedStatusArgs, use_claude_skills: bool, - use_cursor_rules: bool, ) -> Result<()> { println!( "🔍 AI Rules Status for agents: {}, nested_depth: {}", @@ -42,7 +41,7 @@ pub fn run_status( args.nested_depth ); - let status = check_project_status(current_dir, args, use_claude_skills, use_cursor_rules)?; + let status = check_project_status(current_dir, args, use_claude_skills)?; print_status_results(&status); Ok(()) @@ -52,9 +51,8 @@ pub fn check_project_status( current_dir: &Path, args: ResolvedStatusArgs, use_claude_skills: bool, - use_cursor_rules: bool, ) -> Result { - let registry = AgentToolRegistry::new(use_claude_skills, use_cursor_rules); + let registry = AgentToolRegistry::new(use_claude_skills); 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 @@ -289,7 +287,7 @@ Test rule content"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false, false); + let result = check_project_status(temp_dir.path(), args, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -309,7 +307,7 @@ Test rule content"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false, false); + let result = check_project_status(temp_dir.path(), args, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -330,7 +328,7 @@ Test rule content"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false, false); + let result = check_project_status(temp_dir.path(), args, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -357,7 +355,7 @@ Test rule content"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false, false); + let result = check_project_status(temp_dir.path(), args, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -380,7 +378,6 @@ Test rule content"#; nested_depth: NESTED_DEPTH, }, false, - false, ) .unwrap(); @@ -397,7 +394,7 @@ Test rule content"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false, false); + let result = check_project_status(temp_dir.path(), args, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -433,7 +430,7 @@ Test rule content"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false, false); + let result = check_project_status(temp_dir.path(), args, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -466,7 +463,6 @@ Test rule content"#; nested_depth, }, false, - false, ) .unwrap(); @@ -475,7 +471,7 @@ Test rule content"#; command_agents: None, nested_depth, }; - let result = check_project_status(temp_dir.path(), args, false, false); + let result = check_project_status(temp_dir.path(), args, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -488,7 +484,7 @@ Test rule content"#; command_agents: None, nested_depth: 1, }; - let result = check_project_status(temp_dir.path(), args, false, false); + let result = check_project_status(temp_dir.path(), args, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -513,7 +509,6 @@ Test rule content"#; nested_depth: 1, }, false, - false, ) .unwrap(); @@ -522,7 +517,7 @@ Test rule content"#; command_agents: None, nested_depth: 1, }; - let result = check_project_status(temp_dir.path(), args, false, false); + let result = check_project_status(temp_dir.path(), args, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -549,7 +544,6 @@ Test rule content"#; nested_depth: NESTED_DEPTH, }, false, - false, ) .unwrap(); @@ -558,7 +552,7 @@ Test rule content"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false, false); + let result = check_project_status(temp_dir.path(), args, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -594,7 +588,6 @@ Test rule content"#; nested_depth: NESTED_DEPTH, }, false, - false, ) .unwrap(); } @@ -616,7 +609,7 @@ Test rule content"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false, false); + let result = check_project_status(temp_dir.path(), args, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -643,7 +636,7 @@ Test rule content"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false, false); + let result = check_project_status(temp_dir.path(), args, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -669,7 +662,6 @@ Test rule content"#; nested_depth: NESTED_DEPTH, }, false, - false, ); assert!(generate_result.is_ok()); @@ -678,7 +670,7 @@ Test rule content"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false, false); + let result = check_project_status(temp_dir.path(), args, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -712,7 +704,6 @@ Test command body"#; nested_depth: NESTED_DEPTH, }, false, - false, ) .unwrap(); } @@ -740,7 +731,7 @@ Test command body"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false, false); + let result = check_project_status(temp_dir.path(), args, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -773,7 +764,6 @@ Test command body"#; nested_depth: NESTED_DEPTH, }, false, - false, ) .unwrap(); @@ -788,7 +778,7 @@ Test command body"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false, false); + let result = check_project_status(temp_dir.path(), args, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -821,7 +811,6 @@ Test command body"#; nested_depth: NESTED_DEPTH, }, false, - false, ); assert!(generate_result.is_ok()); @@ -839,7 +828,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, false); + let result = check_project_status(temp_dir.path(), args, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -876,7 +865,6 @@ Test command body"#; nested_depth: NESTED_DEPTH, }, false, - false, ); assert!(generate_result.is_ok()); @@ -885,7 +873,7 @@ Test command body"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false, false); + let result = check_project_status(temp_dir.path(), args, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -918,7 +906,6 @@ Test command body"#; nested_depth: NESTED_DEPTH, }, false, - false, ) .unwrap(); @@ -937,7 +924,7 @@ Test command body"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false, false); + let result = check_project_status(temp_dir.path(), args, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -968,7 +955,6 @@ Test command body"#; nested_depth: NESTED_DEPTH, }, false, - false, ); assert!(generate_result.is_ok()); @@ -977,7 +963,7 @@ Test command body"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false, false); + let result = check_project_status(temp_dir.path(), args, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -1004,7 +990,6 @@ Test command body"#; nested_depth: NESTED_DEPTH, }, false, - false, ); assert!(generate_result.is_ok()); @@ -1013,7 +998,7 @@ Test command body"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false, false); + let result = check_project_status(temp_dir.path(), args, false); assert!(result.is_ok()); let status = result.unwrap(); @@ -1045,7 +1030,6 @@ Test command body"#; nested_depth: NESTED_DEPTH, }, false, - false, ); assert!(generate_result.is_ok()); @@ -1057,7 +1041,7 @@ Test command body"#; command_agents: None, nested_depth: NESTED_DEPTH, }; - let result = check_project_status(temp_dir.path(), args, false, false); + let result = check_project_status(temp_dir.path(), args, false); assert!(result.is_ok()); let status = result.unwrap(); diff --git a/src/config.rs b/src/config.rs index 0be15ea..bc02a1c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,8 +12,6 @@ pub struct Config { pub no_gitignore: Option, pub nested_depth: Option, pub use_claude_skills: Option, - #[serde(alias = "useCursorRules", alias = "use_cursor_rule")] - pub use_cursor_rules: Option, } pub fn load_config(current_dir: &Path) -> Result> { @@ -176,70 +174,6 @@ agents: ["claude"] assert!(config.use_claude_skills.is_none()); } - #[test] - fn test_load_config_with_use_cursor_rules() { - let temp_dir = TempDir::new().unwrap(); - let config_content = r#" -agents: ["cursor"] -use_cursor_rules: 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!["cursor".to_string()])); - assert_eq!(config.use_cursor_rules, Some(true)); - } - - #[test] - fn test_load_config_with_use_cursor_rules_camel_case_alias() { - let temp_dir = TempDir::new().unwrap(); - let config_content = r#" -agents: ["cursor"] -useCursorRules: 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.use_cursor_rules, Some(true)); - } - - #[test] - fn test_load_config_with_use_cursor_rules_singular_alias() { - let temp_dir = TempDir::new().unwrap(); - let config_content = r#" -agents: ["cursor"] -use_cursor_rule: 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.use_cursor_rules, Some(true)); - } - - #[test] - fn test_load_config_without_use_cursor_rules_defaults_to_none() { - let temp_dir = TempDir::new().unwrap(); - let config_content = r#" -agents: ["cursor"] -"#; - 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_cursor_rules.is_none()); - } - #[test] fn test_load_config_backward_compatibility_no_gitignore() { let temp_dir = TempDir::new().unwrap(); diff --git a/src/operations/gitignore_updater.rs b/src/operations/gitignore_updater.rs index 217ce0d..037f883 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, false); + let registry = AgentToolRegistry::new(false); 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, false); + let registry = AgentToolRegistry::new(false); 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, false); + let registry = AgentToolRegistry::new(false); let patterns = collect_all_gitignore_patterns(®istry, 1); // Check that skill patterns are included for agents that support skills From 3b4114cb23d70b10459e0f51337adc7a583ae61f Mon Sep 17 00:00:00 2001 From: Jon Andersen Date: Wed, 25 Mar 2026 20:03:33 -0400 Subject: [PATCH 3/4] Remove misplaced Cursor note from config docs Signed-off-by: Jon Andersen --- docs/configuration.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index b63f5fb..2448423 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -32,5 +32,3 @@ 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. - -Cursor uses `AGENTS.md` in standard mode so it shares the same generated content path as other `AGENTS.md`-based agents. From 4539bc8dcfeb6a7ade13dcba9597fb6919956739 Mon Sep 17 00:00:00 2001 From: Jon Andersen Date: Wed, 25 Mar 2026 20:03:36 -0400 Subject: [PATCH 4/4] Remove stale Cursor legacy config docs Signed-off-by: Jon Andersen --- docs/project-structure.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/project-structure.md b/docs/project-structure.md index e6e5890..dc77361 100644 --- a/docs/project-structure.md +++ b/docs/project-structure.md @@ -44,8 +44,6 @@ monorepo/ └── firebender.json # Root Firebender config (generated) ``` -If you need Cursor's legacy `.cursor/rules/*.mdc` output instead, enable `use_cursor_rules: true` in `ai-rules/ai-rules-config.yaml`. - ## Symlink Mode For simple projects with a single `AGENTS.md` file: