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 +1 -1
- package/src/cli.js +84 -3
- package/src/generate.js +205 -7
package/package.json
CHANGED
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.
|
|
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(', ')}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|