cursor-lint 0.7.0 โ 0.9.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 +37 -4
- package/package.json +1 -1
- package/src/cli.js +62 -8
- package/src/generate.js +313 -22
- package/src/versions.js +225 -0
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]
|
|
81
|
-
cursor-lint --
|
|
82
|
-
cursor-lint --
|
|
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) โ
|
|
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
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.
|
|
11
|
+
const VERSION = '0.9.0';
|
|
11
12
|
|
|
12
13
|
const RED = '\x1b[31m';
|
|
13
14
|
const YELLOW = '\x1b[33m';
|
|
@@ -33,6 +34,7 @@ ${YELLOW}Options:${RESET}
|
|
|
33
34
|
--fix Auto-fix common issues (missing frontmatter, alwaysApply)
|
|
34
35
|
--generate Auto-detect stack & download matching .mdc rules from GitHub
|
|
35
36
|
--order Show rule load order, priority tiers, and token estimates
|
|
37
|
+
--version-check Detect installed package versions and show relevant rule tips
|
|
36
38
|
|
|
37
39
|
${YELLOW}What it checks (default):${RESET}
|
|
38
40
|
โข .cursorrules files (warns about agent mode compatibility)
|
|
@@ -90,8 +92,45 @@ async function main() {
|
|
|
90
92
|
const isFix = args.includes('--fix');
|
|
91
93
|
const isGenerate = args.includes('--generate');
|
|
92
94
|
const isOrder = args.includes('--order');
|
|
95
|
+
const isVersionCheck = args.includes('--version-check');
|
|
93
96
|
|
|
94
|
-
if (
|
|
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) {
|
|
95
134
|
const { showLoadOrder } = require('./order');
|
|
96
135
|
console.log(`\n๐ cursor-lint v${VERSION} --order\n`);
|
|
97
136
|
const dir = args.find(a => !a.startsWith('-')) ? path.resolve(args.find(a => !a.startsWith('-'))) : cwd;
|
|
@@ -168,20 +207,35 @@ async function main() {
|
|
|
168
207
|
console.log(`${CYAN}Detected:${RESET} ${results.detected.join(', ')}\n`);
|
|
169
208
|
} else {
|
|
170
209
|
console.log(`${YELLOW}No recognized stack detected.${RESET}`);
|
|
171
|
-
console.log(`${DIM}Supports: package.json, tsconfig.json, requirements.txt,
|
|
210
|
+
console.log(`${DIM}Supports: package.json, tsconfig.json, requirements.txt, pyproject.toml,${RESET}`);
|
|
211
|
+
console.log(`${DIM}Cargo.toml, go.mod, Gemfile, composer.json, pom.xml, build.gradle,${RESET}`);
|
|
212
|
+
console.log(`${DIM}Dockerfile, pubspec.yaml, mix.exs, build.sbt, *.csproj, and more${RESET}\n`);
|
|
172
213
|
process.exit(0);
|
|
173
214
|
}
|
|
174
215
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
216
|
+
const stackCreated = results.created.filter(r => !r.stack.startsWith('best-practice:'));
|
|
217
|
+
const practiceCreated = results.created.filter(r => r.stack.startsWith('best-practice:'));
|
|
218
|
+
const stackSkipped = results.skipped.filter(r => !r.stack.startsWith('best-practice:'));
|
|
219
|
+
const practiceSkipped = results.skipped.filter(r => r.stack.startsWith('best-practice:'));
|
|
220
|
+
|
|
221
|
+
if (stackCreated.length > 0) {
|
|
222
|
+
console.log(`${GREEN}Downloaded (stack rules):${RESET}`);
|
|
223
|
+
for (const r of stackCreated) {
|
|
178
224
|
console.log(` ${GREEN}โ${RESET} .cursor/rules/${r.file} ${DIM}(${r.stack})${RESET}`);
|
|
179
225
|
}
|
|
180
226
|
}
|
|
181
227
|
|
|
182
|
-
if (
|
|
228
|
+
if (practiceCreated.length > 0) {
|
|
229
|
+
console.log(`\n${GREEN}Downloaded (best practices):${RESET}`);
|
|
230
|
+
for (const r of practiceCreated) {
|
|
231
|
+
const label = r.stack.replace('best-practice: ', '');
|
|
232
|
+
console.log(` ${GREEN}โ${RESET} .cursor/rules/${r.file} ${DIM}(${label})${RESET}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (stackSkipped.length + practiceSkipped.length > 0) {
|
|
183
237
|
console.log(`\n${YELLOW}Skipped (already exist):${RESET}`);
|
|
184
|
-
for (const r of
|
|
238
|
+
for (const r of [...stackSkipped, ...practiceSkipped]) {
|
|
185
239
|
console.log(` ${YELLOW}โ ${RESET} .cursor/rules/${r.file}`);
|
|
186
240
|
}
|
|
187
241
|
}
|
package/src/generate.js
CHANGED
|
@@ -4,24 +4,99 @@ const https = require('https');
|
|
|
4
4
|
|
|
5
5
|
const BASE_URL = 'https://raw.githubusercontent.com/cursorrulespacks/cursorrules-collection/main/rules-mdc/';
|
|
6
6
|
|
|
7
|
+
// package.json dependencies โ rule files
|
|
7
8
|
const PKG_DEP_MAP = {
|
|
9
|
+
// Frameworks
|
|
8
10
|
'react': 'frameworks/react.mdc',
|
|
9
11
|
'next': 'frameworks/nextjs.mdc',
|
|
10
12
|
'vue': 'frameworks/vue.mdc',
|
|
13
|
+
'nuxt': 'frameworks/nuxt.mdc',
|
|
11
14
|
'svelte': 'frameworks/svelte.mdc',
|
|
15
|
+
'@sveltejs/kit': 'frameworks/sveltekit.mdc',
|
|
12
16
|
'express': 'frameworks/express.mdc',
|
|
13
17
|
'@nestjs/core': 'frameworks/nestjs.mdc',
|
|
18
|
+
'@angular/core': 'frameworks/angular.mdc',
|
|
19
|
+
'astro': 'frameworks/astro.mdc',
|
|
20
|
+
'gatsby': 'frameworks/gatsby.mdc',
|
|
21
|
+
'remix': 'frameworks/remix.mdc',
|
|
22
|
+
'solid-js': 'frameworks/solid-js.mdc',
|
|
23
|
+
'hono': 'frameworks/hono.mdc',
|
|
24
|
+
'htmx.org': 'frameworks/htmx.mdc',
|
|
25
|
+
'electron': 'frameworks/electron.mdc',
|
|
26
|
+
'@tauri-apps/api': 'frameworks/tauri.mdc',
|
|
27
|
+
'expo': 'frameworks/expo.mdc',
|
|
28
|
+
'swr': 'frameworks/swr.mdc',
|
|
29
|
+
'@tanstack/react-query': 'frameworks/tanstack-query.mdc',
|
|
30
|
+
'zod': 'frameworks/zod.mdc',
|
|
31
|
+
'zustand': 'frameworks/zustand.mdc',
|
|
32
|
+
'@t3-oss/env-nextjs': 'frameworks/t3-stack.mdc',
|
|
33
|
+
'tailwindcss': 'frameworks/tailwind-css.mdc',
|
|
34
|
+
|
|
35
|
+
// Tools
|
|
14
36
|
'prisma': 'tools/prisma.mdc',
|
|
15
37
|
'drizzle-orm': 'tools/drizzle.mdc',
|
|
38
|
+
'@trpc/server': 'tools/trpc.mdc',
|
|
39
|
+
'graphql': 'tools/graphql.mdc',
|
|
40
|
+
'@supabase/supabase-js': 'tools/supabase.mdc',
|
|
41
|
+
'firebase': 'tools/firebase.mdc',
|
|
42
|
+
'convex': 'tools/convex.mdc',
|
|
43
|
+
'@clerk/nextjs': 'tools/clerk.mdc',
|
|
44
|
+
'next-auth': 'tools/nextauth.mdc',
|
|
45
|
+
'stripe': 'tools/stripe.mdc',
|
|
46
|
+
'@langchain/core': 'tools/langchain.mdc',
|
|
47
|
+
'mongodb': 'tools/mongodb.mdc',
|
|
48
|
+
'redis': 'tools/redis.mdc',
|
|
49
|
+
'jest': 'tools/jest.mdc',
|
|
50
|
+
'vitest': 'tools/vitest.mdc',
|
|
51
|
+
'cypress': 'tools/cypress.mdc',
|
|
52
|
+
'@playwright/test': 'tools/playwright.mdc',
|
|
53
|
+
'@storybook/react': 'tools/storybook.mdc',
|
|
54
|
+
'turborepo': 'tools/turborepo.mdc',
|
|
55
|
+
'bun': 'tools/bun.mdc',
|
|
16
56
|
};
|
|
17
57
|
|
|
18
|
-
|
|
58
|
+
// Python requirements.txt / pyproject.toml โ rule files
|
|
59
|
+
const PY_DEP_MAP = {
|
|
19
60
|
'django': 'frameworks/django.mdc',
|
|
20
61
|
'fastapi': 'frameworks/fastapi.mdc',
|
|
21
62
|
'flask': 'frameworks/flask.mdc',
|
|
22
63
|
'pydantic': 'tools/pydantic.mdc',
|
|
23
64
|
'sqlalchemy': 'tools/sqlalchemy.mdc',
|
|
24
65
|
'pytest': 'tools/pytest.mdc',
|
|
66
|
+
'langchain': 'tools/langchain.mdc',
|
|
67
|
+
'ruff': 'tools/ruff.mdc',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Gemfile โ rule files
|
|
71
|
+
const RUBY_DEP_MAP = {
|
|
72
|
+
'rails': 'frameworks/rails.mdc',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// composer.json โ rule files
|
|
76
|
+
const PHP_DEP_MAP = {
|
|
77
|
+
'laravel/framework': 'frameworks/laravel.mdc',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// build.gradle / pom.xml โ rule files
|
|
81
|
+
const JVM_DEP_MAP = {
|
|
82
|
+
'spring-boot': 'frameworks/spring-boot.mdc',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Best practices auto-included when certain conditions are met
|
|
86
|
+
const PRACTICE_TRIGGERS = {
|
|
87
|
+
// Always suggest these for any project with >3 detected deps
|
|
88
|
+
'practices/clean-code.mdc': { minDeps: 3, label: 'clean-code' },
|
|
89
|
+
'practices/error-handling.mdc': { minDeps: 3, label: 'error-handling' },
|
|
90
|
+
'practices/git-workflow.mdc': { files: ['.git'], label: 'git-workflow' },
|
|
91
|
+
'practices/testing.mdc': { deps: ['jest', 'vitest', 'cypress', '@playwright/test', 'pytest', 'mocha', 'ava'], label: 'testing' },
|
|
92
|
+
'practices/security.mdc': { minDeps: 5, label: 'security' },
|
|
93
|
+
'practices/documentation.mdc': { files: ['README.md'], label: 'documentation' },
|
|
94
|
+
'practices/api-design.mdc': { deps: ['express', '@nestjs/core', 'fastapi', 'flask', 'django', 'hono', '@trpc/server', 'graphql'], label: 'api-design' },
|
|
95
|
+
'practices/performance.mdc': { deps: ['react', 'next', 'vue', 'nuxt', '@angular/core', 'svelte'], label: 'performance' },
|
|
96
|
+
'practices/database-migrations.mdc': { deps: ['prisma', 'drizzle-orm', 'sqlalchemy', 'django'], label: 'database-migrations' },
|
|
97
|
+
'practices/monorepo.mdc': { files: ['pnpm-workspace.yaml', 'lerna.json'], deps: ['turborepo'], label: 'monorepo' },
|
|
98
|
+
'practices/accessibility.mdc': { deps: ['react', 'next', 'vue', '@angular/core', 'svelte'], label: 'accessibility' },
|
|
99
|
+
'practices/logging.mdc': { deps: ['express', '@nestjs/core', 'fastapi', 'flask', 'django', 'hono'], label: 'logging' },
|
|
25
100
|
};
|
|
26
101
|
|
|
27
102
|
function fetchFile(url) {
|
|
@@ -47,74 +122,290 @@ function fetchFile(url) {
|
|
|
47
122
|
});
|
|
48
123
|
}
|
|
49
124
|
|
|
125
|
+
function readPyDeps(cwd) {
|
|
126
|
+
const deps = [];
|
|
127
|
+
|
|
128
|
+
// requirements.txt
|
|
129
|
+
const reqPath = path.join(cwd, 'requirements.txt');
|
|
130
|
+
if (fs.existsSync(reqPath)) {
|
|
131
|
+
try {
|
|
132
|
+
const content = fs.readFileSync(reqPath, 'utf8').toLowerCase();
|
|
133
|
+
for (const dep of Object.keys(PY_DEP_MAP)) {
|
|
134
|
+
if (content.includes(dep)) deps.push(dep);
|
|
135
|
+
}
|
|
136
|
+
} catch {}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// pyproject.toml (rough match)
|
|
140
|
+
const pyprojectPath = path.join(cwd, 'pyproject.toml');
|
|
141
|
+
if (fs.existsSync(pyprojectPath)) {
|
|
142
|
+
try {
|
|
143
|
+
const content = fs.readFileSync(pyprojectPath, 'utf8').toLowerCase();
|
|
144
|
+
for (const dep of Object.keys(PY_DEP_MAP)) {
|
|
145
|
+
if (content.includes(dep)) deps.push(dep);
|
|
146
|
+
}
|
|
147
|
+
} catch {}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return [...new Set(deps)];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function readRubyDeps(cwd) {
|
|
154
|
+
const gemfilePath = path.join(cwd, 'Gemfile');
|
|
155
|
+
if (!fs.existsSync(gemfilePath)) return [];
|
|
156
|
+
try {
|
|
157
|
+
const content = fs.readFileSync(gemfilePath, 'utf8').toLowerCase();
|
|
158
|
+
return Object.keys(RUBY_DEP_MAP).filter(dep => content.includes(dep));
|
|
159
|
+
} catch { return []; }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function readPhpDeps(cwd) {
|
|
163
|
+
const composerPath = path.join(cwd, 'composer.json');
|
|
164
|
+
if (!fs.existsSync(composerPath)) return [];
|
|
165
|
+
try {
|
|
166
|
+
const pkg = JSON.parse(fs.readFileSync(composerPath, 'utf8'));
|
|
167
|
+
const allDeps = { ...pkg.require, ...pkg['require-dev'] };
|
|
168
|
+
return Object.keys(PHP_DEP_MAP).filter(dep => allDeps[dep]);
|
|
169
|
+
} catch { return []; }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function readJvmDeps(cwd) {
|
|
173
|
+
const deps = [];
|
|
174
|
+
for (const file of ['build.gradle', 'build.gradle.kts', 'pom.xml']) {
|
|
175
|
+
const p = path.join(cwd, file);
|
|
176
|
+
if (fs.existsSync(p)) {
|
|
177
|
+
try {
|
|
178
|
+
const content = fs.readFileSync(p, 'utf8').toLowerCase();
|
|
179
|
+
for (const dep of Object.keys(JVM_DEP_MAP)) {
|
|
180
|
+
if (content.includes(dep)) deps.push(dep);
|
|
181
|
+
}
|
|
182
|
+
} catch {}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return [...new Set(deps)];
|
|
186
|
+
}
|
|
187
|
+
|
|
50
188
|
function detectStack(cwd) {
|
|
51
189
|
const detected = [];
|
|
52
190
|
const rules = new Map(); // rulePath -> stackName
|
|
191
|
+
const allDetectedDeps = [];
|
|
53
192
|
|
|
54
193
|
// package.json
|
|
55
194
|
const pkgPath = path.join(cwd, 'package.json');
|
|
195
|
+
let pkgDeps = {};
|
|
56
196
|
if (fs.existsSync(pkgPath)) {
|
|
57
197
|
try {
|
|
58
198
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
59
|
-
|
|
199
|
+
pkgDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
60
200
|
for (const [dep, rule] of Object.entries(PKG_DEP_MAP)) {
|
|
61
|
-
if (
|
|
201
|
+
if (pkgDeps[dep]) {
|
|
62
202
|
detected.push(dep);
|
|
203
|
+
allDetectedDeps.push(dep);
|
|
63
204
|
rules.set(rule, dep);
|
|
64
205
|
}
|
|
65
206
|
}
|
|
66
207
|
} catch {}
|
|
67
208
|
}
|
|
68
209
|
|
|
69
|
-
// tsconfig.json
|
|
210
|
+
// tsconfig.json โ TypeScript
|
|
70
211
|
if (fs.existsSync(path.join(cwd, 'tsconfig.json'))) {
|
|
71
212
|
detected.push('TypeScript');
|
|
72
213
|
rules.set('languages/typescript.mdc', 'TypeScript');
|
|
73
214
|
}
|
|
74
215
|
|
|
216
|
+
// JavaScript (package.json exists but no TS)
|
|
217
|
+
if (fs.existsSync(pkgPath) && !fs.existsSync(path.join(cwd, 'tsconfig.json'))) {
|
|
218
|
+
detected.push('JavaScript');
|
|
219
|
+
rules.set('languages/javascript.mdc', 'JavaScript');
|
|
220
|
+
}
|
|
221
|
+
|
|
75
222
|
// Python
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
223
|
+
const hasPyFile = (() => { try { return fs.readdirSync(cwd).some(f => f.endsWith('.py')); } catch { return false; } })();
|
|
224
|
+
const hasPyProject = fs.existsSync(path.join(cwd, 'requirements.txt')) ||
|
|
225
|
+
fs.existsSync(path.join(cwd, 'pyproject.toml')) ||
|
|
226
|
+
fs.existsSync(path.join(cwd, 'setup.py')) ||
|
|
227
|
+
hasPyFile;
|
|
228
|
+
if (hasPyProject) {
|
|
79
229
|
detected.push('Python');
|
|
80
230
|
rules.set('languages/python.mdc', 'Python');
|
|
231
|
+
const pyDeps = readPyDeps(cwd);
|
|
232
|
+
for (const dep of pyDeps) {
|
|
233
|
+
detected.push(dep);
|
|
234
|
+
allDetectedDeps.push(dep);
|
|
235
|
+
rules.set(PY_DEP_MAP[dep], dep);
|
|
236
|
+
}
|
|
81
237
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
238
|
+
|
|
239
|
+
// Ruby
|
|
240
|
+
if (fs.existsSync(path.join(cwd, 'Gemfile'))) {
|
|
241
|
+
detected.push('Ruby');
|
|
242
|
+
rules.set('languages/ruby.mdc', 'Ruby');
|
|
243
|
+
for (const dep of readRubyDeps(cwd)) {
|
|
244
|
+
detected.push(dep);
|
|
245
|
+
allDetectedDeps.push(dep);
|
|
246
|
+
rules.set(RUBY_DEP_MAP[dep], dep);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// PHP
|
|
251
|
+
if (fs.existsSync(path.join(cwd, 'composer.json'))) {
|
|
252
|
+
detected.push('PHP');
|
|
253
|
+
rules.set('languages/php.mdc', 'PHP');
|
|
254
|
+
for (const dep of readPhpDeps(cwd)) {
|
|
255
|
+
detected.push(dep);
|
|
256
|
+
allDetectedDeps.push(dep);
|
|
257
|
+
rules.set(PHP_DEP_MAP[dep], dep);
|
|
258
|
+
}
|
|
92
259
|
}
|
|
93
260
|
|
|
94
|
-
//
|
|
261
|
+
// Rust
|
|
95
262
|
if (fs.existsSync(path.join(cwd, 'Cargo.toml'))) {
|
|
96
263
|
detected.push('Rust');
|
|
97
264
|
rules.set('languages/rust.mdc', 'Rust');
|
|
98
265
|
}
|
|
99
266
|
|
|
100
|
-
//
|
|
267
|
+
// Go
|
|
101
268
|
if (fs.existsSync(path.join(cwd, 'go.mod'))) {
|
|
102
269
|
detected.push('Go');
|
|
103
270
|
rules.set('languages/go.mdc', 'Go');
|
|
104
271
|
}
|
|
105
272
|
|
|
106
|
-
//
|
|
107
|
-
if (fs.existsSync(path.join(cwd, '
|
|
273
|
+
// Java
|
|
274
|
+
if (fs.existsSync(path.join(cwd, 'pom.xml')) || fs.existsSync(path.join(cwd, 'build.gradle')) || fs.existsSync(path.join(cwd, 'build.gradle.kts'))) {
|
|
275
|
+
detected.push('Java');
|
|
276
|
+
rules.set('languages/java.mdc', 'Java');
|
|
277
|
+
for (const dep of readJvmDeps(cwd)) {
|
|
278
|
+
detected.push(dep);
|
|
279
|
+
allDetectedDeps.push(dep);
|
|
280
|
+
rules.set(JVM_DEP_MAP[dep], dep);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Kotlin
|
|
285
|
+
if (fs.existsSync(path.join(cwd, 'build.gradle.kts'))) {
|
|
286
|
+
detected.push('Kotlin');
|
|
287
|
+
rules.set('languages/kotlin.mdc', 'Kotlin');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Swift
|
|
291
|
+
if (fs.existsSync(path.join(cwd, 'Package.swift'))) {
|
|
292
|
+
detected.push('Swift');
|
|
293
|
+
rules.set('languages/swift.mdc', 'Swift');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Elixir
|
|
297
|
+
if (fs.existsSync(path.join(cwd, 'mix.exs'))) {
|
|
298
|
+
detected.push('Elixir');
|
|
299
|
+
rules.set('languages/elixir.mdc', 'Elixir');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Scala
|
|
303
|
+
if (fs.existsSync(path.join(cwd, 'build.sbt'))) {
|
|
304
|
+
detected.push('Scala');
|
|
305
|
+
rules.set('languages/scala.mdc', 'Scala');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// C#
|
|
309
|
+
const hasCsproj = (() => { try { return fs.readdirSync(cwd).some(f => f.endsWith('.csproj') || f.endsWith('.sln')); } catch { return false; } })();
|
|
310
|
+
if (hasCsproj) {
|
|
311
|
+
detected.push('C#');
|
|
312
|
+
rules.set('languages/csharp.mdc', 'C#');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// C++
|
|
316
|
+
if (fs.existsSync(path.join(cwd, 'CMakeLists.txt')) || fs.existsSync(path.join(cwd, 'Makefile'))) {
|
|
317
|
+
const hasCpp = (() => { try { return fs.readdirSync(cwd).some(f => /\.(cpp|cc|cxx|hpp|h)$/.test(f)); } catch { return false; } })();
|
|
318
|
+
if (hasCpp) {
|
|
319
|
+
detected.push('C++');
|
|
320
|
+
rules.set('languages/cpp.mdc', 'C++');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Flutter (pubspec.yaml)
|
|
325
|
+
if (fs.existsSync(path.join(cwd, 'pubspec.yaml'))) {
|
|
326
|
+
detected.push('Flutter');
|
|
327
|
+
rules.set('frameworks/flutter.mdc', 'Flutter');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Docker
|
|
331
|
+
if (fs.existsSync(path.join(cwd, 'Dockerfile')) || fs.existsSync(path.join(cwd, 'docker-compose.yml')) || fs.existsSync(path.join(cwd, 'docker-compose.yaml'))) {
|
|
108
332
|
detected.push('Docker');
|
|
109
333
|
rules.set('tools/docker.mdc', 'Docker');
|
|
110
334
|
}
|
|
111
335
|
|
|
336
|
+
// Kubernetes
|
|
337
|
+
const k8sDir = path.join(cwd, 'k8s');
|
|
338
|
+
if (fs.existsSync(k8sDir) || fs.existsSync(path.join(cwd, 'kubernetes'))) {
|
|
339
|
+
detected.push('Kubernetes');
|
|
340
|
+
rules.set('tools/kubernetes.mdc', 'Kubernetes');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Terraform
|
|
344
|
+
const hasTf = (() => { try { return fs.readdirSync(cwd).some(f => f.endsWith('.tf')); } catch { return false; } })();
|
|
345
|
+
if (hasTf) {
|
|
346
|
+
detected.push('Terraform');
|
|
347
|
+
rules.set('tools/terraform.mdc', 'Terraform');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Deno
|
|
351
|
+
if (fs.existsSync(path.join(cwd, 'deno.json')) || fs.existsSync(path.join(cwd, 'deno.jsonc'))) {
|
|
352
|
+
detected.push('Deno');
|
|
353
|
+
rules.set('tools/deno.mdc', 'Deno');
|
|
354
|
+
}
|
|
355
|
+
|
|
112
356
|
// CI/CD
|
|
113
|
-
if (fs.existsSync(path.join(cwd, '.github', 'workflows'))) {
|
|
357
|
+
if (fs.existsSync(path.join(cwd, '.github', 'workflows')) || fs.existsSync(path.join(cwd, '.gitlab-ci.yml'))) {
|
|
114
358
|
detected.push('CI/CD');
|
|
115
359
|
rules.set('tools/ci-cd.mdc', 'CI/CD');
|
|
116
360
|
}
|
|
117
361
|
|
|
362
|
+
// Nginx
|
|
363
|
+
const hasNginx = (() => { try { return fs.readdirSync(cwd).some(f => f.includes('nginx')); } catch { return false; } })();
|
|
364
|
+
if (hasNginx) {
|
|
365
|
+
detected.push('Nginx');
|
|
366
|
+
rules.set('tools/nginx.mdc', 'Nginx');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// SQLite
|
|
370
|
+
if (pkgDeps['better-sqlite3'] || pkgDeps['sqlite3']) {
|
|
371
|
+
detected.push('SQLite');
|
|
372
|
+
allDetectedDeps.push('sqlite3');
|
|
373
|
+
rules.set('tools/sqlite.mdc', 'SQLite');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// PostgreSQL
|
|
377
|
+
if (pkgDeps['pg'] || pkgDeps['postgres']) {
|
|
378
|
+
detected.push('PostgreSQL');
|
|
379
|
+
allDetectedDeps.push('pg');
|
|
380
|
+
rules.set('tools/postgresql.mdc', 'PostgreSQL');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// AWS
|
|
384
|
+
if (pkgDeps['@aws-sdk/client-s3'] || pkgDeps['aws-sdk'] || fs.existsSync(path.join(cwd, 'serverless.yml')) || fs.existsSync(path.join(cwd, 'template.yaml'))) {
|
|
385
|
+
detected.push('AWS');
|
|
386
|
+
allDetectedDeps.push('aws-sdk');
|
|
387
|
+
rules.set('tools/aws.mdc', 'AWS');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Best practices โ auto-suggest based on project signals
|
|
391
|
+
for (const [rulePath, trigger] of Object.entries(PRACTICE_TRIGGERS)) {
|
|
392
|
+
let shouldInclude = false;
|
|
393
|
+
|
|
394
|
+
if (trigger.minDeps && allDetectedDeps.length >= trigger.minDeps) {
|
|
395
|
+
shouldInclude = true;
|
|
396
|
+
}
|
|
397
|
+
if (trigger.deps && trigger.deps.some(d => allDetectedDeps.includes(d) || pkgDeps[d])) {
|
|
398
|
+
shouldInclude = true;
|
|
399
|
+
}
|
|
400
|
+
if (trigger.files && trigger.files.some(f => fs.existsSync(path.join(cwd, f)))) {
|
|
401
|
+
shouldInclude = true;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (shouldInclude) {
|
|
405
|
+
rules.set(rulePath, `best-practice: ${trigger.label}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
118
409
|
return { detected, rules };
|
|
119
410
|
}
|
|
120
411
|
|
package/src/versions.js
ADDED
|
@@ -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 };
|