cursor-lint 0.8.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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +22 -7
  3. package/src/generate.js +313 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-lint",
3
- "version": "0.8.0",
3
+ "version": "0.9.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
@@ -8,7 +8,7 @@ const { fixProject } = require('./fix');
8
8
  const { generateRules } = require('./generate');
9
9
  const { checkVersions, checkRuleVersionMismatches } = require('./versions');
10
10
 
11
- const VERSION = '0.8.0';
11
+ const VERSION = '0.9.0';
12
12
 
13
13
  const RED = '\x1b[31m';
14
14
  const YELLOW = '\x1b[33m';
@@ -207,20 +207,35 @@ async function main() {
207
207
  console.log(`${CYAN}Detected:${RESET} ${results.detected.join(', ')}\n`);
208
208
  } else {
209
209
  console.log(`${YELLOW}No recognized stack detected.${RESET}`);
210
- console.log(`${DIM}Supports: package.json, tsconfig.json, requirements.txt, Cargo.toml, go.mod, Dockerfile${RESET}\n`);
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`);
211
213
  process.exit(0);
212
214
  }
213
215
 
214
- if (results.created.length > 0) {
215
- console.log(`${GREEN}Downloaded:${RESET}`);
216
- for (const r of results.created) {
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) {
217
224
  console.log(` ${GREEN}✓${RESET} .cursor/rules/${r.file} ${DIM}(${r.stack})${RESET}`);
218
225
  }
219
226
  }
220
227
 
221
- if (results.skipped.length > 0) {
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) {
222
237
  console.log(`\n${YELLOW}Skipped (already exist):${RESET}`);
223
- for (const r of results.skipped) {
238
+ for (const r of [...stackSkipped, ...practiceSkipped]) {
224
239
  console.log(` ${YELLOW}⚠${RESET} .cursor/rules/${r.file}`);
225
240
  }
226
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
- const REQ_DEP_MAP = {
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
- const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
199
+ pkgDeps = { ...pkg.dependencies, ...pkg.devDependencies };
60
200
  for (const [dep, rule] of Object.entries(PKG_DEP_MAP)) {
61
- if (allDeps[dep]) {
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 reqPath = path.join(cwd, 'requirements.txt');
77
- const hasPy = fs.existsSync(reqPath) || fs.readdirSync(cwd).some(f => f.endsWith('.py'));
78
- if (hasPy) {
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
- if (fs.existsSync(reqPath)) {
83
- try {
84
- const req = fs.readFileSync(reqPath, 'utf8').toLowerCase();
85
- for (const [dep, rule] of Object.entries(REQ_DEP_MAP)) {
86
- if (req.includes(dep)) {
87
- detected.push(dep);
88
- rules.set(rule, dep);
89
- }
90
- }
91
- } catch {}
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
- // Cargo.toml
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
- // go.mod
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
- // Dockerfile
107
- if (fs.existsSync(path.join(cwd, 'Dockerfile'))) {
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