cursor-lint 0.12.0 → 0.13.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-lint",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Lint your Cursor rules — catch common mistakes before they break your workflow",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -5,10 +5,10 @@ const { lintProject } = require('./index');
5
5
  const { verifyProject } = require('./verify');
6
6
  const { initProject } = require('./init');
7
7
  const { fixProject } = require('./fix');
8
- const { generateRules, suggestSkills } = require('./generate');
8
+ const { generateRules, suggestSkills, listPresets, generateFromPreset } = require('./generate');
9
9
  const { checkVersions, checkRuleVersionMismatches } = require('./versions');
10
10
 
11
- const VERSION = '0.12.0';
11
+ const VERSION = '0.13.0';
12
12
 
13
13
  const RED = '\x1b[31m';
14
14
  const YELLOW = '\x1b[33m';
@@ -34,6 +34,8 @@ ${YELLOW}Options:${RESET}
34
34
  --init Generate starter .mdc rules (auto-detects your stack)
35
35
  --fix Auto-fix common issues (missing frontmatter, alwaysApply)
36
36
  --generate Auto-detect stack & download matching .mdc rules from GitHub
37
+ --generate --preset <name> Install rules for a popular stack preset
38
+ --generate --preset list Show available presets
37
39
  --order Show rule load order, priority tiers, and token estimates
38
40
  --version-check Detect installed package versions and show relevant rule tips
39
41
 
@@ -208,13 +210,85 @@ async function main() {
208
210
  process.exit(0);
209
211
 
210
212
  } else if (isGenerate) {
213
+ // Check for --preset flag
214
+ const presetIndex = args.indexOf('--preset');
215
+ const hasPreset = presetIndex !== -1;
216
+ const presetValue = hasPreset ? args[presetIndex + 1] : null;
217
+
218
+ // Handle --preset list
219
+ if (hasPreset && presetValue === 'list') {
220
+ console.log(`\nšŸš€ cursor-lint v${VERSION} --generate --preset list\n`);
221
+ console.log(`${CYAN}Available presets:${RESET}\n`);
222
+
223
+ const presets = listPresets();
224
+ for (const [key, preset] of Object.entries(presets)) {
225
+ const paddedKey = key.padEnd(12);
226
+ console.log(` ${GREEN}${paddedKey}${RESET} ${preset.name} — ${DIM}${preset.description}${RESET}`);
227
+ }
228
+
229
+ console.log(`\n${YELLOW}Usage:${RESET} cursor-lint --generate --preset t3\n`);
230
+ process.exit(0);
231
+ }
232
+
233
+ // Handle --preset <name>
234
+ if (hasPreset && presetValue && presetValue !== 'list') {
235
+ console.log(`\nšŸš€ cursor-lint v${VERSION} --generate --preset ${presetValue}\n`);
236
+
237
+ const presets = listPresets();
238
+ if (!presets[presetValue]) {
239
+ console.log(`${RED}Unknown preset: ${presetValue}${RESET}\n`);
240
+ console.log(`Run ${CYAN}cursor-lint --generate --preset list${RESET} to see available presets\n`);
241
+ process.exit(1);
242
+ }
243
+
244
+ const results = await generateFromPreset(cwd, presetValue);
245
+
246
+ console.log(`${CYAN}Preset:${RESET} ${results.presetInfo.name}`);
247
+ console.log(`${DIM}${results.presetInfo.description}${RESET}\n`);
248
+
249
+ if (results.created.length > 0) {
250
+ console.log(`${GREEN}Downloaded:${RESET}`);
251
+ for (const r of results.created) {
252
+ console.log(` ${GREEN}āœ“${RESET} .cursor/rules/${r.file}`);
253
+ }
254
+ }
255
+
256
+ if (results.skipped.length > 0) {
257
+ console.log(`\n${YELLOW}Skipped (already exist):${RESET}`);
258
+ for (const r of results.skipped) {
259
+ console.log(` ${YELLOW}⚠${RESET} .cursor/rules/${r.file}`);
260
+ }
261
+ }
262
+
263
+ if (results.failed.length > 0) {
264
+ console.log(`\n${RED}Failed:${RESET}`);
265
+ for (const r of results.failed) {
266
+ console.log(` ${RED}āœ—${RESET} ${r.file} — ${r.error}`);
267
+ }
268
+ }
269
+
270
+ if (results.created.length > 0) {
271
+ console.log(`\n${DIM}Run cursor-lint to check these rules${RESET}\n`);
272
+ }
273
+
274
+ process.exit(results.failed.length > 0 ? 1 : 0);
275
+ }
276
+
277
+ // Regular --generate (no preset)
211
278
  console.log(`\nšŸš€ cursor-lint v${VERSION} --generate\n`);
212
279
  console.log(`Detecting stack in ${cwd}...\n`);
213
280
 
214
281
  const results = await generateRules(cwd);
215
282
 
216
283
  if (results.detected.length > 0) {
217
- console.log(`${CYAN}Detected:${RESET} ${results.detected.join(', ')}\n`);
284
+ console.log(`${CYAN}Detected stack:${RESET} ${results.detected.join(', ')}`);
285
+
286
+ // Show versions if available
287
+ if (results.versions && Object.keys(results.versions).length > 0) {
288
+ const versionStrs = Object.entries(results.versions).map(([dep, ver]) => `${dep}@${ver}`);
289
+ console.log(`${CYAN}Versions:${RESET} ${versionStrs.join(', ')}`);
290
+ }
291
+ console.log();
218
292
  } else {
219
293
  console.log(`${YELLOW}No recognized stack detected.${RESET}`);
220
294
  console.log(`${DIM}Supports: package.json, tsconfig.json, requirements.txt, pyproject.toml,${RESET}`);
@@ -250,6 +324,13 @@ async function main() {
250
324
  }
251
325
  }
252
326
 
327
+ if (results.fallbacks && results.fallbacks.length > 0) {
328
+ console.log(`\n${YELLOW}Fallbacks (version-specific rule not found):${RESET}`);
329
+ for (const r of results.fallbacks) {
330
+ console.log(` ${YELLOW}⚠${RESET} ${r.from} → ${r.to} ${DIM}(${r.stack})${RESET}`);
331
+ }
332
+ }
333
+
253
334
  if (results.failed.length > 0) {
254
335
  console.log(`\n${RED}Failed:${RESET}`);
255
336
  for (const r of results.failed) {
package/src/generate.js CHANGED
@@ -4,6 +4,32 @@ const https = require('https');
4
4
 
5
5
  const BASE_URL = 'https://raw.githubusercontent.com/nedcodes-ok/cursorrules-collection/main/rules-mdc/';
6
6
 
7
+ // Version-specific rule overrides: if user has react 19, use react-19.mdc instead of react.mdc
8
+ const VERSION_RULES = {
9
+ 'react': {
10
+ '19': 'frameworks/react-19.mdc',
11
+ '18': 'frameworks/react-18.mdc',
12
+ },
13
+ 'next': {
14
+ '15': 'frameworks/nextjs-15.mdc',
15
+ '14': 'frameworks/nextjs-14.mdc',
16
+ '13': 'frameworks/nextjs-13.mdc',
17
+ },
18
+ 'vue': {
19
+ '3': 'frameworks/vue-3.mdc',
20
+ '2': 'frameworks/vue-2.mdc',
21
+ },
22
+ '@angular/core': {
23
+ '19': 'frameworks/angular-19.mdc',
24
+ '18': 'frameworks/angular-18.mdc',
25
+ '17': 'frameworks/angular-17.mdc',
26
+ },
27
+ 'svelte': {
28
+ '5': 'frameworks/svelte-5.mdc',
29
+ '4': 'frameworks/svelte-4.mdc',
30
+ },
31
+ };
32
+
7
33
  // package.json dependencies → rule files
8
34
  const PKG_DEP_MAP = {
9
35
  // Frameworks
@@ -189,6 +215,7 @@ function detectStack(cwd) {
189
215
  const detected = [];
190
216
  const rules = new Map(); // rulePath -> stackName
191
217
  const allDetectedDeps = [];
218
+ const versions = {}; // dep -> version string
192
219
 
193
220
  // package.json
194
221
  const pkgPath = path.join(cwd, 'package.json');
@@ -201,7 +228,21 @@ function detectStack(cwd) {
201
228
  if (pkgDeps[dep]) {
202
229
  detected.push(dep);
203
230
  allDetectedDeps.push(dep);
204
- rules.set(rule, dep);
231
+
232
+ // Extract version
233
+ const rawVersion = pkgDeps[dep];
234
+ versions[dep] = rawVersion;
235
+
236
+ // Parse major version (strip ^, ~, >=, etc.)
237
+ const versionMatch = rawVersion.match(/(\d+)/);
238
+ const majorVersion = versionMatch ? versionMatch[1] : null;
239
+
240
+ // Check if version-specific rule exists
241
+ if (majorVersion && VERSION_RULES[dep] && VERSION_RULES[dep][majorVersion]) {
242
+ rules.set(VERSION_RULES[dep][majorVersion], dep);
243
+ } else {
244
+ rules.set(rule, dep);
245
+ }
205
246
  }
206
247
  }
207
248
  } catch {}
@@ -406,18 +447,19 @@ function detectStack(cwd) {
406
447
  }
407
448
  }
408
449
 
409
- return { detected, rules };
450
+ return { detected, rules, versions };
410
451
  }
411
452
 
412
453
  async function generateRules(cwd) {
413
- const { detected, rules } = detectStack(cwd);
454
+ const { detected, rules, versions } = detectStack(cwd);
414
455
  const rulesDir = path.join(cwd, '.cursor', 'rules');
415
456
  const created = [];
416
457
  const skipped = [];
417
458
  const failed = [];
459
+ const fallbacks = [];
418
460
 
419
461
  if (rules.size === 0) {
420
- return { detected, created, skipped, failed };
462
+ return { detected, created, skipped, failed, versions };
421
463
  }
422
464
 
423
465
  fs.mkdirSync(rulesDir, { recursive: true });
@@ -437,13 +479,130 @@ async function generateRules(cwd) {
437
479
  fs.writeFileSync(destPath, content, 'utf8');
438
480
  created.push({ file: filename, stack: stackName });
439
481
  } catch (err) {
440
- failed.push({ file: filename, stack: stackName, error: err.message });
482
+ // Try fallback to generic rule if this was a version-specific rule
483
+ const genericRule = PKG_DEP_MAP[stackName];
484
+ if (genericRule && genericRule !== rulePath) {
485
+ try {
486
+ const fallbackUrl = BASE_URL + genericRule;
487
+ const content = await fetchFile(fallbackUrl);
488
+ const genericFilename = path.basename(genericRule);
489
+ const genericDestPath = path.join(rulesDir, genericFilename);
490
+ fs.writeFileSync(genericDestPath, content, 'utf8');
491
+ created.push({ file: genericFilename, stack: stackName });
492
+ fallbacks.push({ from: filename, to: genericFilename, stack: stackName });
493
+ } catch (fallbackErr) {
494
+ failed.push({ file: filename, stack: stackName, error: err.message });
495
+ }
496
+ } else {
497
+ failed.push({ file: filename, stack: stackName, error: err.message });
498
+ }
441
499
  }
442
500
  }
443
501
 
444
- return { detected, created, skipped, failed };
502
+ return { detected, created, skipped, failed, versions, fallbacks };
445
503
  }
446
504
 
505
+ const STACK_PRESETS = {
506
+ 't3': {
507
+ name: 'T3 Stack',
508
+ description: 'Next.js + TypeScript + Tailwind + tRPC + Prisma + NextAuth',
509
+ rules: [
510
+ 'languages/typescript.mdc',
511
+ 'frameworks/nextjs.mdc',
512
+ 'frameworks/tailwind-css.mdc',
513
+ 'frameworks/zod.mdc',
514
+ 'frameworks/t3-stack.mdc',
515
+ 'tools/trpc.mdc',
516
+ 'tools/prisma.mdc',
517
+ 'tools/nextauth.mdc',
518
+ 'practices/clean-code.mdc',
519
+ 'practices/error-handling.mdc',
520
+ ],
521
+ },
522
+ 'mern': {
523
+ name: 'MERN Stack',
524
+ description: 'MongoDB + Express + React + Node.js',
525
+ rules: [
526
+ 'languages/typescript.mdc',
527
+ 'languages/javascript.mdc',
528
+ 'frameworks/react.mdc',
529
+ 'frameworks/express.mdc',
530
+ 'tools/mongodb.mdc',
531
+ 'practices/api-design.mdc',
532
+ 'practices/error-handling.mdc',
533
+ 'practices/clean-code.mdc',
534
+ ],
535
+ },
536
+ 'fastapi': {
537
+ name: 'Python FastAPI',
538
+ description: 'FastAPI + Pydantic + SQLAlchemy + pytest',
539
+ rules: [
540
+ 'languages/python.mdc',
541
+ 'frameworks/fastapi.mdc',
542
+ 'tools/pydantic.mdc',
543
+ 'tools/sqlalchemy.mdc',
544
+ 'tools/pytest.mdc',
545
+ 'practices/api-design.mdc',
546
+ 'practices/error-handling.mdc',
547
+ 'practices/clean-code.mdc',
548
+ ],
549
+ },
550
+ 'sveltekit': {
551
+ name: 'SvelteKit Full Stack',
552
+ description: 'SvelteKit + Svelte + Tailwind + Prisma + TypeScript',
553
+ rules: [
554
+ 'languages/typescript.mdc',
555
+ 'frameworks/sveltekit.mdc',
556
+ 'frameworks/svelte.mdc',
557
+ 'frameworks/tailwind-css.mdc',
558
+ 'tools/prisma.mdc',
559
+ 'practices/clean-code.mdc',
560
+ 'practices/error-handling.mdc',
561
+ ],
562
+ },
563
+ 'rails': {
564
+ name: 'Ruby on Rails',
565
+ description: 'Rails + Ruby + PostgreSQL + Testing',
566
+ rules: [
567
+ 'languages/ruby.mdc',
568
+ 'frameworks/rails.mdc',
569
+ 'tools/postgresql.mdc',
570
+ 'practices/testing.mdc',
571
+ 'practices/api-design.mdc',
572
+ 'practices/database-migrations.mdc',
573
+ 'practices/clean-code.mdc',
574
+ ],
575
+ },
576
+ 'nextjs': {
577
+ name: 'Next.js Full Stack',
578
+ description: 'Next.js + React + TypeScript + Tailwind + Prisma',
579
+ rules: [
580
+ 'languages/typescript.mdc',
581
+ 'frameworks/nextjs.mdc',
582
+ 'frameworks/react.mdc',
583
+ 'frameworks/tailwind-css.mdc',
584
+ 'tools/prisma.mdc',
585
+ 'practices/clean-code.mdc',
586
+ 'practices/performance.mdc',
587
+ 'practices/error-handling.mdc',
588
+ ],
589
+ },
590
+ 'django': {
591
+ name: 'Django Full Stack',
592
+ description: 'Django + Python + PostgreSQL + pytest',
593
+ rules: [
594
+ 'languages/python.mdc',
595
+ 'frameworks/django.mdc',
596
+ 'tools/postgresql.mdc',
597
+ 'tools/pytest.mdc',
598
+ 'practices/api-design.mdc',
599
+ 'practices/database-migrations.mdc',
600
+ 'practices/security.mdc',
601
+ 'practices/clean-code.mdc',
602
+ ],
603
+ },
604
+ };
605
+
447
606
  const SKILLS_API = 'https://skills.sh/api/search';
448
607
 
449
608
  function searchSkillsAPI(query, limit) {
@@ -497,4 +656,43 @@ async function suggestSkills(detected) {
497
656
  return allResults.slice(0, 10);
498
657
  }
499
658
 
500
- module.exports = { generateRules, suggestSkills };
659
+ function listPresets() {
660
+ return STACK_PRESETS;
661
+ }
662
+
663
+ async function generateFromPreset(cwd, presetName) {
664
+ const preset = STACK_PRESETS[presetName];
665
+ if (!preset) {
666
+ throw new Error(`Unknown preset: ${presetName}`);
667
+ }
668
+
669
+ const rulesDir = path.join(cwd, '.cursor', 'rules');
670
+ const created = [];
671
+ const skipped = [];
672
+ const failed = [];
673
+
674
+ fs.mkdirSync(rulesDir, { recursive: true });
675
+
676
+ for (const rulePath of preset.rules) {
677
+ const filename = path.basename(rulePath);
678
+ const destPath = path.join(rulesDir, filename);
679
+
680
+ if (fs.existsSync(destPath)) {
681
+ skipped.push({ file: filename, rule: rulePath });
682
+ continue;
683
+ }
684
+
685
+ try {
686
+ const url = BASE_URL + rulePath;
687
+ const content = await fetchFile(url);
688
+ fs.writeFileSync(destPath, content, 'utf8');
689
+ created.push({ file: filename, rule: rulePath });
690
+ } catch (err) {
691
+ failed.push({ file: filename, rule: rulePath, error: err.message });
692
+ }
693
+ }
694
+
695
+ return { preset: presetName, presetInfo: preset, created, skipped, failed };
696
+ }
697
+
698
+ module.exports = { generateRules, suggestSkills, listPresets, generateFromPreset };