cursor-lint 0.6.0 โ†’ 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -77,9 +77,42 @@ cursor-lint exits with code 1 when errors are found. Add it to your pipeline:
77
77
  ## Options
78
78
 
79
79
  ```
80
- cursor-lint [directory] Lint rules in directory (default: current dir)
81
- cursor-lint --help Show help
82
- cursor-lint --version Show version
80
+ cursor-lint [directory] Lint rules in directory (default: current dir)
81
+ cursor-lint --fix Auto-fix common issues (missing frontmatter, alwaysApply)
82
+ cursor-lint --generate Auto-detect stack & download matching rules from collection
83
+ cursor-lint --verify Check if code follows rules with verify: blocks
84
+ cursor-lint --order Show rule load order, priority tiers, and token estimates
85
+ cursor-lint --version-check Detect installed versions, show relevant features & rule mismatches
86
+ cursor-lint --init Generate starter rules (auto-detects your stack)
87
+ cursor-lint --help Show help
88
+ cursor-lint --version Show version
89
+ ```
90
+
91
+ ### --version-check
92
+
93
+ Reads your `package.json`, `requirements.txt`, or `pyproject.toml` and tells you:
94
+ 1. **Version-specific features** available in your installed packages (e.g., "React 19+: use useActionState")
95
+ 2. **Rule mismatches** โ€” if your `.mdc` rules reference version features your installed packages don't support
96
+
97
+ ```bash
98
+ npx cursor-lint --version-check
99
+ ```
100
+
101
+ ```
102
+ ๐Ÿ“ฆ cursor-lint v0.8.0 --version-check
103
+
104
+ Version-specific features available:
105
+
106
+ react (^19.0.0)
107
+ โ†’ React 19+: use useActionState (replaces useFormState), use() hook
108
+ โ†’ React 18+: useId, useSyncExternalStore, automatic batching
109
+
110
+ next (^14.2.0)
111
+ โ†’ Next.js 14+: Server Actions stable, partial prerendering (preview)
112
+
113
+ Version mismatches in your rules:
114
+
115
+ โš  nextjs.mdc:5 โ€” Rule references 15+ but next ^14.2.0 is installed
83
116
  ```
84
117
 
85
118
  ## Based on Real Testing
@@ -102,6 +135,6 @@ Made by [nedcodes](https://dev.to/nedcodes) ยท [Free rules collection](https://g
102
135
 
103
136
  ## Related
104
137
 
105
- - [cursorrules-collection](https://github.com/cursorrulespacks/cursorrules-collection) โ€” 77+ free .mdc rules
138
+ - [cursorrules-collection](https://github.com/cursorrulespacks/cursorrules-collection) โ€” 104 free .mdc rules
106
139
  - [Cursor Setup Audit](https://cursorrulespacks.gumroad.com/l/cursor-setup-audit) โ€” Professional review of your rules setup ($50)
107
140
  - [Articles on Dev.to](https://dev.to/nedcodes) โ€” Guides on writing effective Cursor rules
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-lint",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Lint your Cursor rules \u2014 catch common mistakes before they break your workflow",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -6,8 +6,9 @@ const { verifyProject } = require('./verify');
6
6
  const { initProject } = require('./init');
7
7
  const { fixProject } = require('./fix');
8
8
  const { generateRules } = require('./generate');
9
+ const { checkVersions, checkRuleVersionMismatches } = require('./versions');
9
10
 
10
- const VERSION = '0.6.0';
11
+ const VERSION = '0.8.0';
11
12
 
12
13
  const RED = '\x1b[31m';
13
14
  const YELLOW = '\x1b[33m';
@@ -32,6 +33,8 @@ ${YELLOW}Options:${RESET}
32
33
  --init Generate starter .mdc rules (auto-detects your stack)
33
34
  --fix Auto-fix common issues (missing frontmatter, alwaysApply)
34
35
  --generate Auto-detect stack & download matching .mdc rules from GitHub
36
+ --order Show rule load order, priority tiers, and token estimates
37
+ --version-check Detect installed package versions and show relevant rule tips
35
38
 
36
39
  ${YELLOW}What it checks (default):${RESET}
37
40
  โ€ข .cursorrules files (warns about agent mode compatibility)
@@ -88,8 +91,113 @@ async function main() {
88
91
  const isInit = args.includes('--init');
89
92
  const isFix = args.includes('--fix');
90
93
  const isGenerate = args.includes('--generate');
94
+ const isOrder = args.includes('--order');
95
+ const isVersionCheck = args.includes('--version-check');
91
96
 
92
- if (isGenerate) {
97
+ if (isVersionCheck) {
98
+ console.log(`\n๐Ÿ“ฆ cursor-lint v${VERSION} --version-check\n`);
99
+ console.log(`Detecting installed versions in ${cwd}...\n`);
100
+
101
+ const versionNotes = checkVersions(cwd);
102
+ const mismatches = checkRuleVersionMismatches(cwd);
103
+
104
+ if (versionNotes.length === 0 && mismatches.length === 0) {
105
+ console.log(`${YELLOW}No version-specific notes found.${RESET}`);
106
+ console.log(`${DIM}Supports: package.json, requirements.txt, pyproject.toml${RESET}\n`);
107
+ process.exit(0);
108
+ }
109
+
110
+ if (versionNotes.length > 0) {
111
+ console.log(`${CYAN}Version-specific features available:${RESET}\n`);
112
+ for (const item of versionNotes) {
113
+ console.log(` ${GREEN}${item.package}${RESET} ${DIM}(${item.installedVersion})${RESET}`);
114
+ for (const note of item.notes) {
115
+ console.log(` ${DIM}โ†’${RESET} ${note}`);
116
+ }
117
+ console.log();
118
+ }
119
+ }
120
+
121
+ if (mismatches.length > 0) {
122
+ console.log(`${YELLOW}Version mismatches in your rules:${RESET}\n`);
123
+ for (const m of mismatches) {
124
+ console.log(` ${YELLOW}โš ${RESET} ${m.file}:${m.line} โ€” ${m.message}`);
125
+ }
126
+ console.log();
127
+ }
128
+
129
+ console.log('โ”€'.repeat(50));
130
+ console.log(`${DIM}Use these notes to customize your .mdc rules for your exact versions.${RESET}\n`);
131
+ process.exit(mismatches.length > 0 ? 1 : 0);
132
+
133
+ } else if (isOrder) {
134
+ const { showLoadOrder } = require('./order');
135
+ console.log(`\n๐Ÿ“‹ cursor-lint v${VERSION} --order\n`);
136
+ const dir = args.find(a => !a.startsWith('-')) ? path.resolve(args.find(a => !a.startsWith('-'))) : cwd;
137
+ console.log(`Analyzing rule load order in ${dir}...\n`);
138
+
139
+ const results = showLoadOrder(dir);
140
+
141
+ if (results.rules.length === 0) {
142
+ console.log(`${YELLOW}No rules found.${RESET}\n`);
143
+ process.exit(0);
144
+ }
145
+
146
+ // Show .cursorrules warning if present
147
+ if (results.hasCursorrules) {
148
+ console.log(`${YELLOW}โš  .cursorrules found${RESET} โ€” overridden by any .mdc rule covering the same topic`);
149
+ console.log(`${DIM} .mdc files always take precedence when both exist${RESET}\n`);
150
+ }
151
+
152
+ // Group by priority tier
153
+ const tiers = {
154
+ 'always': { label: 'Always Active', color: GREEN, rules: [] },
155
+ 'glob': { label: 'File-Scoped (glob match)', color: CYAN, rules: [] },
156
+ 'manual': { label: 'Manual Only (no alwaysApply, no globs)', color: DIM, rules: [] },
157
+ };
158
+
159
+ for (const rule of results.rules) {
160
+ tiers[rule.tier].rules.push(rule);
161
+ }
162
+
163
+ let position = 1;
164
+ for (const [key, tier] of Object.entries(tiers)) {
165
+ if (tier.rules.length === 0) continue;
166
+ console.log(`${tier.color}โ”€โ”€ ${tier.label} โ”€โ”€${RESET}`);
167
+ for (const rule of tier.rules) {
168
+ const globs = rule.globs.length > 0 ? ` ${DIM}[${rule.globs.join(', ')}]${RESET}` : '';
169
+ const desc = rule.description ? ` ${DIM}โ€” ${rule.description}${RESET}` : '';
170
+ const size = ` ${DIM}(${rule.lines} lines, ~${rule.tokens} tokens)${RESET}`;
171
+ console.log(` ${position}. ${rule.file}${globs}${desc}${size}`);
172
+ position++;
173
+ }
174
+ console.log();
175
+ }
176
+
177
+ // Token budget warning
178
+ const totalTokens = results.rules.reduce((s, r) => s + r.tokens, 0);
179
+ const alwaysTokens = tiers.always.rules.reduce((s, r) => s + r.tokens, 0);
180
+ console.log('โ”€'.repeat(50));
181
+ console.log(`${CYAN}Total rules:${RESET} ${results.rules.length}`);
182
+ console.log(`${CYAN}Always-active token estimate:${RESET} ~${alwaysTokens} tokens`);
183
+ console.log(`${CYAN}All rules token estimate:${RESET} ~${totalTokens} tokens`);
184
+
185
+ if (alwaysTokens > 4000) {
186
+ console.log(`\n${YELLOW}โš  Your always-active rules use ~${alwaysTokens} tokens.${RESET}`);
187
+ console.log(`${DIM} Large rule sets eat into your context window. Consider moving some to glob-scoped rules.${RESET}`);
188
+ }
189
+
190
+ if (results.warnings.length > 0) {
191
+ console.log();
192
+ for (const w of results.warnings) {
193
+ console.log(`${YELLOW}โš  ${w}${RESET}`);
194
+ }
195
+ }
196
+
197
+ console.log();
198
+ process.exit(0);
199
+
200
+ } else if (isGenerate) {
93
201
  console.log(`\n๐Ÿš€ cursor-lint v${VERSION} --generate\n`);
94
202
  console.log(`Detecting stack in ${cwd}...\n`);
95
203
 
package/src/order.js ADDED
@@ -0,0 +1,131 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ function parseFrontmatter(content) {
5
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
6
+ if (!match) return { found: false, data: null };
7
+
8
+ const data = {};
9
+ const lines = match[1].split('\n');
10
+ for (const line of lines) {
11
+ const colonIdx = line.indexOf(':');
12
+ if (colonIdx === -1) continue;
13
+ const key = line.slice(0, colonIdx).trim();
14
+ const rawVal = line.slice(colonIdx + 1).trim();
15
+ if (rawVal === 'true') data[key] = true;
16
+ else if (rawVal === 'false') data[key] = false;
17
+ else if (rawVal.startsWith('"') && rawVal.endsWith('"')) data[key] = rawVal.slice(1, -1);
18
+ else data[key] = rawVal;
19
+ }
20
+ return { found: true, data };
21
+ }
22
+
23
+ function parseGlobs(globVal) {
24
+ if (!globVal) return [];
25
+ if (typeof globVal === 'string') {
26
+ const trimmed = globVal.trim();
27
+ if (trimmed.startsWith('[')) {
28
+ return trimmed.slice(1, -1).split(',').map(g => g.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
29
+ }
30
+ return trimmed.split(',').map(g => g.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
31
+ }
32
+ return [];
33
+ }
34
+
35
+ function estimateTokens(text) {
36
+ // Rough estimate: ~4 chars per token for English text
37
+ return Math.ceil(text.length / 4);
38
+ }
39
+
40
+ function showLoadOrder(dir) {
41
+ const results = {
42
+ hasCursorrules: false,
43
+ rules: [],
44
+ warnings: [],
45
+ };
46
+
47
+ // Check for .cursorrules
48
+ const cursorrules = path.join(dir, '.cursorrules');
49
+ if (fs.existsSync(cursorrules)) {
50
+ results.hasCursorrules = true;
51
+ const content = fs.readFileSync(cursorrules, 'utf-8');
52
+ const lines = content.split('\n').length;
53
+ results.rules.push({
54
+ file: '.cursorrules',
55
+ tier: 'always',
56
+ globs: [],
57
+ description: '(legacy format)',
58
+ alwaysApply: true,
59
+ lines,
60
+ tokens: estimateTokens(content),
61
+ priority: 0, // lowest priority โ€” overridden by .mdc
62
+ });
63
+ }
64
+
65
+ // Check .cursor/rules/*.mdc
66
+ const rulesDir = path.join(dir, '.cursor', 'rules');
67
+ if (fs.existsSync(rulesDir) && fs.statSync(rulesDir).isDirectory()) {
68
+ const files = fs.readdirSync(rulesDir).filter(f => f.endsWith('.mdc')).sort();
69
+
70
+ for (const file of files) {
71
+ const filePath = path.join(rulesDir, file);
72
+ const content = fs.readFileSync(filePath, 'utf-8');
73
+ const fm = parseFrontmatter(content);
74
+ const lines = content.split('\n').length;
75
+ const tokens = estimateTokens(content);
76
+
77
+ if (!fm.found || !fm.data) {
78
+ results.rules.push({
79
+ file,
80
+ tier: 'manual',
81
+ globs: [],
82
+ description: '(no frontmatter)',
83
+ alwaysApply: false,
84
+ lines,
85
+ tokens,
86
+ });
87
+ results.warnings.push(`${file}: Missing frontmatter โ€” rule may not load at all`);
88
+ continue;
89
+ }
90
+
91
+ const globs = parseGlobs(fm.data.globs);
92
+ const alwaysApply = fm.data.alwaysApply === true;
93
+ const description = fm.data.description || '';
94
+
95
+ let tier;
96
+ if (alwaysApply) {
97
+ tier = 'always';
98
+ } else if (globs.length > 0) {
99
+ tier = 'glob';
100
+ } else {
101
+ tier = 'manual';
102
+ results.warnings.push(`${file}: No alwaysApply and no globs โ€” this rule may never activate in agent mode`);
103
+ }
104
+
105
+ results.rules.push({
106
+ file,
107
+ tier,
108
+ globs,
109
+ description,
110
+ alwaysApply,
111
+ lines,
112
+ tokens,
113
+ });
114
+ }
115
+ }
116
+
117
+ // Sort within tiers: always first, then glob, then manual
118
+ // Within each tier, sort by filename (alphabetical = filesystem order)
119
+ const tierOrder = { always: 0, glob: 1, manual: 2 };
120
+ results.rules.sort((a, b) => {
121
+ if (tierOrder[a.tier] !== tierOrder[b.tier]) return tierOrder[a.tier] - tierOrder[b.tier];
122
+ // .cursorrules always last within 'always' tier (lowest priority)
123
+ if (a.file === '.cursorrules') return -1;
124
+ if (b.file === '.cursorrules') return 1;
125
+ return a.file.localeCompare(b.file);
126
+ });
127
+
128
+ return results;
129
+ }
130
+
131
+ module.exports = { showLoadOrder };
@@ -0,0 +1,225 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ // Map of package names to their version-specific rule notes
5
+ // Each entry: { package, minVersion, note }
6
+ const VERSION_NOTES = [
7
+ // React
8
+ { package: 'react', minVersion: '19.0.0', note: 'React 19+: use useActionState (replaces useFormState), use() hook for promises/context' },
9
+ { package: 'react', minVersion: '18.0.0', note: 'React 18+: useId, useSyncExternalStore, automatic batching, Suspense for data fetching' },
10
+
11
+ // Next.js
12
+ { package: 'next', minVersion: '15.0.0', note: 'Next.js 15+: async request APIs (cookies/headers/params are now async), Turbopack stable' },
13
+ { package: 'next', minVersion: '14.0.0', note: 'Next.js 14+: Server Actions stable, partial prerendering (preview), Metadata API improvements' },
14
+ { package: 'next', minVersion: '13.4.0', note: 'Next.js 13.4+: App Router stable, Server Components default. Pages Router is legacy' },
15
+
16
+ // Vue
17
+ { package: 'vue', minVersion: '3.4.0', note: 'Vue 3.4+: defineModel(), improved reactivity, v-bind shorthand' },
18
+ { package: 'vue', minVersion: '3.3.0', note: 'Vue 3.3+: generic components, defineSlots, defineOptions' },
19
+
20
+ // Angular
21
+ { package: '@angular/core', minVersion: '18.0.0', note: 'Angular 18+: stable signals, zoneless change detection (experimental), @let template syntax' },
22
+ { package: '@angular/core', minVersion: '17.0.0', note: 'Angular 17+: new control flow (@if/@for/@switch), deferrable views (@defer), signal inputs/outputs' },
23
+
24
+ // Prisma
25
+ { package: 'prisma', minVersion: '5.0.0', note: 'Prisma 5+: JSON protocol default, improved query engine, Prisma Client extensions stable' },
26
+ { package: '@prisma/client', minVersion: '5.0.0', note: 'Prisma 5+: $extends replaces middleware (deprecated), improved type safety' },
27
+
28
+ // Tailwind
29
+ { package: 'tailwindcss', minVersion: '4.0.0', note: 'Tailwind v4+: CSS-first config, no tailwind.config.js needed, @theme directive, automatic content detection' },
30
+ { package: 'tailwindcss', minVersion: '3.4.0', note: 'Tailwind 3.4+: size-* utility (replaces w-* h-* pairs), has-* and group-has-* variants' },
31
+ { package: 'tailwindcss', minVersion: '3.3.0', note: 'Tailwind 3.3+: ESM config support, logical properties, overflow-clip utility' },
32
+
33
+ // TypeScript
34
+ { package: 'typescript', minVersion: '5.5.0', note: 'TypeScript 5.5+: inferred type predicates, config extends from multiple files' },
35
+ { package: 'typescript', minVersion: '5.0.0', note: 'TypeScript 5+: decorators (TC39 standard), const type parameters, --moduleResolution bundler' },
36
+
37
+ // Express
38
+ { package: 'express', minVersion: '5.0.0', note: 'Express 5+: async error handling built-in (no more express-async-errors), path route matching changes' },
39
+
40
+ // Pydantic (Python)
41
+ { package: 'pydantic', minVersion: '2.0.0', note: 'Pydantic v2+: model_validator/field_validator replace validator/root_validator, ConfigDict replaces Config class, 5-50x faster' },
42
+
43
+ // FastAPI (Python)
44
+ { package: 'fastapi', minVersion: '0.100.0', note: 'FastAPI 0.100+: Annotated dependencies preferred, Pydantic v2 support, lifespan replaces on_event' },
45
+
46
+ // Django (Python)
47
+ { package: 'django', minVersion: '5.0', note: 'Django 5+: GeneratedField, Field.db_default, facet filters in admin, simplified templates' },
48
+ { package: 'django', minVersion: '4.2', note: 'Django 4.2+: psycopg 3 support, comments on columns/tables, custom file storage' },
49
+ ];
50
+
51
+ /**
52
+ * Parse a semver-ish string into comparable parts.
53
+ * Handles: "5.0.0", "^5.0.0", "~5.0.0", ">=5.0.0", "5.0", "5"
54
+ */
55
+ function parseVersion(v) {
56
+ if (!v) return null;
57
+ const cleaned = v.replace(/^[\^~>=<]+/, '').trim();
58
+ const parts = cleaned.split('.').map(Number);
59
+ return {
60
+ major: parts[0] || 0,
61
+ minor: parts[1] || 0,
62
+ patch: parts[2] || 0,
63
+ };
64
+ }
65
+
66
+ function versionGte(installed, required) {
67
+ const a = parseVersion(installed);
68
+ const b = parseVersion(required);
69
+ if (!a || !b) return false;
70
+ if (a.major !== b.major) return a.major > b.major;
71
+ if (a.minor !== b.minor) return a.minor > b.minor;
72
+ return a.patch >= b.patch;
73
+ }
74
+
75
+ /**
76
+ * Detect installed versions from package.json and Python config files.
77
+ * Returns Map<packageName, versionString>
78
+ */
79
+ function detectVersions(cwd) {
80
+ const versions = new Map();
81
+
82
+ // package.json
83
+ const pkgPath = path.join(cwd, 'package.json');
84
+ if (fs.existsSync(pkgPath)) {
85
+ try {
86
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
87
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
88
+ for (const [name, version] of Object.entries(allDeps)) {
89
+ versions.set(name, version);
90
+ }
91
+ } catch {}
92
+ }
93
+
94
+ // pyproject.toml (basic parsing)
95
+ const pyprojectPath = path.join(cwd, 'pyproject.toml');
96
+ if (fs.existsSync(pyprojectPath)) {
97
+ try {
98
+ const content = fs.readFileSync(pyprojectPath, 'utf8');
99
+ // Match lines like: django = ">=4.2" or django = {version = ">=4.2"}
100
+ const depRegex = /^(\w[\w-]*)\s*=\s*"([^"]+)"/gm;
101
+ let match;
102
+ while ((match = depRegex.exec(content)) !== null) {
103
+ versions.set(match[1].toLowerCase(), match[2]);
104
+ }
105
+ } catch {}
106
+ }
107
+
108
+ // requirements.txt
109
+ const reqPath = path.join(cwd, 'requirements.txt');
110
+ if (fs.existsSync(reqPath)) {
111
+ try {
112
+ const lines = fs.readFileSync(reqPath, 'utf8').split('\n');
113
+ for (const line of lines) {
114
+ const trimmed = line.trim();
115
+ if (!trimmed || trimmed.startsWith('#')) continue;
116
+ // Match: django>=4.2, django==4.2.0, django~=4.2
117
+ const match = trimmed.match(/^([\w-]+)\s*([><=~!]+)\s*([\d.]+)/);
118
+ if (match) {
119
+ versions.set(match[1].toLowerCase(), match[3]);
120
+ }
121
+ }
122
+ } catch {}
123
+ }
124
+
125
+ return versions;
126
+ }
127
+
128
+ /**
129
+ * Check installed versions against version notes.
130
+ * Returns array of { package, installedVersion, notes[] }
131
+ */
132
+ function checkVersions(cwd) {
133
+ const versions = detectVersions(cwd);
134
+ const results = [];
135
+
136
+ // Group notes by package
137
+ const notesByPkg = new Map();
138
+ for (const note of VERSION_NOTES) {
139
+ if (!notesByPkg.has(note.package)) notesByPkg.set(note.package, []);
140
+ notesByPkg.get(note.package).push(note);
141
+ }
142
+
143
+ for (const [pkg, notes] of notesByPkg) {
144
+ const installed = versions.get(pkg);
145
+ if (!installed) continue;
146
+
147
+ const applicable = [];
148
+ for (const note of notes) {
149
+ if (versionGte(installed, note.minVersion)) {
150
+ applicable.push(note.note);
151
+ }
152
+ }
153
+
154
+ if (applicable.length > 0) {
155
+ results.push({
156
+ package: pkg,
157
+ installedVersion: installed,
158
+ notes: applicable,
159
+ });
160
+ }
161
+ }
162
+
163
+ return results;
164
+ }
165
+
166
+ /**
167
+ * Scan .mdc rules for version references that don't match installed versions.
168
+ * Returns array of { file, line, message }
169
+ */
170
+ function checkRuleVersionMismatches(cwd) {
171
+ const versions = detectVersions(cwd);
172
+ const warnings = [];
173
+ const rulesDir = path.join(cwd, '.cursor', 'rules');
174
+
175
+ if (!fs.existsSync(rulesDir)) return warnings;
176
+
177
+ const files = fs.readdirSync(rulesDir).filter(f => f.endsWith('.mdc'));
178
+
179
+ for (const file of files) {
180
+ const content = fs.readFileSync(path.join(rulesDir, file), 'utf8');
181
+ const lines = content.split('\n');
182
+
183
+ for (let i = 0; i < lines.length; i++) {
184
+ const line = lines[i];
185
+
186
+ // Check for version references like "v14+", "v3.4+", "(v5+)", "17+"
187
+ const versionRefs = line.matchAll(/\b(?:v|version\s*)?([\d]+(?:\.[\d]+)*)\+/gi);
188
+ for (const match of versionRefs) {
189
+ const refVersion = match[1];
190
+ // Try to find which package this might relate to based on the file name
191
+ const fileBase = file.replace('.mdc', '').toLowerCase();
192
+
193
+ // Map filenames to package names
194
+ const fileToPackage = {
195
+ 'nextjs': 'next',
196
+ 'react': 'react',
197
+ 'vue': 'vue',
198
+ 'angular': '@angular/core',
199
+ 'tailwind-css': 'tailwindcss',
200
+ 'typescript': 'typescript',
201
+ 'prisma': 'prisma',
202
+ 'express': 'express',
203
+ 'django': 'django',
204
+ 'fastapi': 'fastapi',
205
+ };
206
+
207
+ const pkg = fileToPackage[fileBase];
208
+ if (pkg && versions.has(pkg)) {
209
+ const installed = versions.get(pkg);
210
+ if (!versionGte(installed, refVersion)) {
211
+ warnings.push({
212
+ file,
213
+ line: i + 1,
214
+ message: `Rule references ${refVersion}+ but ${pkg} ${installed} is installed`,
215
+ });
216
+ }
217
+ }
218
+ }
219
+ }
220
+ }
221
+
222
+ return warnings;
223
+ }
224
+
225
+ module.exports = { detectVersions, checkVersions, checkRuleVersionMismatches, parseVersion, versionGte };