Skip to content

Commit 2b86e2f

Browse files
committed
fix: align Cursor rule extension handling
1 parent c0efa10 commit 2b86e2f

7 files changed

Lines changed: 57 additions & 19 deletions

File tree

.github/workflows/mcp-release-watch.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ jobs:
6464
if [[ -z "$latest_release" ]]; then
6565
# No final release, select the newest final date tag instead of
6666
# blindly accepting an RC/final-suffixed tag at index zero.
67-
if ! latest_release=$(gh api "repos/$MCP_REPO/tags" --jq '[.[].name | select(test("^[0-9]{4}-[0-9]{2}-[0-9]{2}$"))][0] // empty' 2>/dev/null); then
67+
if ! latest_release=$(gh api "repos/$MCP_REPO/tags" --jq '[.[].name | select(test("^[0-9]{4}-[0-9]{2}-[0-9]{2}$"))] | max // empty' 2>/dev/null); then
6868
echo "ERROR: Failed to query MCP releases and tags"
6969
exit 1
7070
fi

crates/agnix-cli/src/watch.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ where
6161
fn is_relevant_file(path: &Path) -> bool {
6262
let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
6363
let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("");
64+
let is_cursor_rule = matches!(
65+
agnix_core::detect_file_type(path),
66+
agnix_core::FileType::CursorRule
67+
);
6468
let parent = path
6569
.parent()
6670
.and_then(|p| p.file_name())
@@ -91,7 +95,8 @@ fn is_relevant_file(path: &Path) -> bool {
9195
) || extension == "mcp"
9296
|| is_codex_config
9397
|| filename.ends_with(".mcp.json")
94-
|| filename.ends_with(".mdc")
98+
|| extension.eq_ignore_ascii_case("mdc")
99+
|| is_cursor_rule
95100
|| filename.ends_with(".instructions.md")
96101
// Also watch for agent files
97102
|| (extension == "md"
@@ -126,4 +131,18 @@ mod tests {
126131
assert!(!is_relevant_file(Path::new("configs/config.yaml")));
127132
assert!(!is_relevant_file(Path::new("configs/config.json")));
128133
}
134+
135+
#[test]
136+
fn cursor_rule_files_are_relevant() {
137+
for path in [
138+
".cursor/rules/test.md",
139+
".cursor/rules/test.MD",
140+
".cursor/rules/test.mdc",
141+
".cursor/rules/test.MDC",
142+
] {
143+
assert!(is_relevant_file(Path::new(path)));
144+
}
145+
146+
assert!(!is_relevant_file(Path::new("docs/test.md")));
147+
}
129148
}

crates/agnix-core/src/rules/claude_md.rs

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@ fn is_cursor_rules_file(path: &Path) -> bool {
4444
return true;
4545
}
4646

47-
(filename.ends_with(".md") || filename.ends_with(".mdc")) && is_path_under_cursor_rules(path)
47+
path.extension()
48+
.and_then(|extension| extension.to_str())
49+
.is_some_and(|extension| {
50+
extension.eq_ignore_ascii_case("md") || extension.eq_ignore_ascii_case("mdc")
51+
})
52+
&& is_path_under_cursor_rules(path)
4853
}
4954

5055
impl Validator for ClaudeMdValidator {
@@ -404,28 +409,29 @@ mod tests {
404409
fn test_cursor_rules_mdc_gets_rules() {
405410
let content = "Be helpful and accurate when responding.";
406411
let validator = ClaudeMdValidator;
407-
let diagnostics = validator.validate(
408-
Path::new(".cursor/rules/typescript.mdc"),
409-
content,
410-
&LintConfig::default(),
411-
);
412412

413-
assert!(!diagnostics.is_empty());
414-
assert!(diagnostics.iter().any(|d| d.rule == "CC-MEM-005"));
413+
for path in [
414+
".cursor/rules/typescript.mdc",
415+
".cursor/rules/typescript.MDC",
416+
] {
417+
let diagnostics = validator.validate(Path::new(path), content, &LintConfig::default());
418+
419+
assert!(!diagnostics.is_empty());
420+
assert!(diagnostics.iter().any(|d| d.rule == "CC-MEM-005"));
421+
}
415422
}
416423

417424
#[test]
418425
fn test_cursor_rules_md_gets_rules() {
419426
let content = "Be helpful and accurate when responding.";
420427
let validator = ClaudeMdValidator;
421-
let diagnostics = validator.validate(
422-
Path::new(".cursor/rules/typescript.md"),
423-
content,
424-
&LintConfig::default(),
425-
);
426428

427-
assert!(!diagnostics.is_empty());
428-
assert!(diagnostics.iter().any(|d| d.rule == "CC-MEM-005"));
429+
for path in [".cursor/rules/typescript.md", ".cursor/rules/typescript.MD"] {
430+
let diagnostics = validator.validate(Path::new(path), content, &LintConfig::default());
431+
432+
assert!(!diagnostics.is_empty());
433+
assert!(diagnostics.iter().any(|d| d.rule == "CC-MEM-005"));
434+
}
429435
}
430436

431437
#[test]

crates/agnix-core/src/rules/cursor.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -996,7 +996,7 @@ fn validate_cursor_environment_file(
996996
}
997997
}
998998

999-
for field in ["$schema", "name", "user", "snapshot"] {
999+
for field in ["name", "user", "snapshot"] {
10001000
if root.get(field).is_some_and(|value| !value.is_string()) {
10011001
diagnostics.push(
10021002
cursor_environment_error(
@@ -2492,7 +2492,6 @@ is_background: false
24922492

24932493
for (name, content) in [
24942494
("unknown root field", r#"{"unknown":true}"#),
2495-
("invalid schema association", r#"{"$schema":42}"#),
24962495
("invalid name", r#"{"name":1}"#),
24972496
("invalid user", r#"{"user":false}"#),
24982497
(

crates/agnix-lsp/src/backend/helpers.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ impl Backend {
7878
|| file_name.eq_ignore_ascii_case("copilot-instructions.md")
7979
|| file_name.to_lowercase().ends_with(".instructions.md")
8080
|| file_name.to_lowercase().ends_with(".mdc")
81+
|| matches!(
82+
agnix_core::detect_file_type(path),
83+
agnix_core::FileType::CursorRule
84+
)
8185
|| file_name.eq_ignore_ascii_case("opencode.json")
8286
|| file_name.eq_ignore_ascii_case("opencode.jsonc")
8387
}

crates/agnix-lsp/src/backend/tests.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2063,6 +2063,12 @@ fn test_is_project_level_trigger() {
20632063
assert!(Backend::is_project_level_trigger(Path::new(
20642064
"/project/.cursor/rules/test.mdc"
20652065
)));
2066+
assert!(Backend::is_project_level_trigger(Path::new(
2067+
"/project/.cursor/rules/test.MDC"
2068+
)));
2069+
assert!(Backend::is_project_level_trigger(Path::new(
2070+
"/project/.cursor/rules/test.MD"
2071+
)));
20662072
assert!(Backend::is_project_level_trigger(Path::new(
20672073
"/project/GEMINI.md"
20682074
)));

tests/ci_workflow.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ fn mcp_release_watch_filters_prerelease_tags() {
5656
workflow.contains(r#"select(test("^[0-9]{4}-[0-9]{2}-[0-9]{2}$"))"#),
5757
"MCP tag fallback must select only final date-based versions"
5858
);
59+
assert!(
60+
workflow.contains("] | max // empty"),
61+
"MCP tag fallback must select the newest final date-based version"
62+
);
5963
assert!(
6064
workflow.contains("Ignoring non-final MCP release/tag"),
6165
"MCP prerelease tags must be ignored instead of failing the workflow"

0 commit comments

Comments
 (0)