cursor-lint 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nedcodes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # cursor-lint
2
+
3
+ Lint your [Cursor](https://cursor.com) rules. Catch common mistakes before they silently break your workflow.
4
+
5
+ ```bash
6
+ npx cursor-lint
7
+ ```
8
+
9
+ ```
10
+ 🔍 cursor-lint v0.1.0
11
+
12
+ .cursorrules
13
+ ⚠ .cursorrules may be ignored in agent mode
14
+ → Use .cursor/rules/*.mdc with alwaysApply: true
15
+
16
+ .cursor/rules/code.mdc
17
+ ✗ Missing alwaysApply: true
18
+ → Add alwaysApply: true to frontmatter for agent mode
19
+ ⚠ Vague rule detected: "write clean code" (line 6)
20
+
21
+ .cursor/rules/memory.mdc
22
+ ✓ All checks passed
23
+
24
+ ──────────────────────────────────────────────────
25
+ 1 error, 2 warnings, 1 passed
26
+ ```
27
+
28
+ ## Why
29
+
30
+ Cursor rules fail silently. You won't know your rules are broken until the AI ignores them. Common mistakes:
31
+
32
+ - Using `.cursorrules` instead of `.mdc` files (agent mode ignores `.cursorrules`)
33
+ - Missing `alwaysApply: true` (rules never load)
34
+ - Vague instructions ("write clean code") that have zero effect
35
+ - Files too long for the context window
36
+ - Bad YAML frontmatter or glob syntax
37
+
38
+ cursor-lint catches all of these in seconds.
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ # Run directly (no install)
44
+ npx cursor-lint
45
+
46
+ # Or install globally
47
+ npm install -g cursor-lint
48
+ ```
49
+
50
+ ## What It Checks
51
+
52
+ | Check | Severity | Description |
53
+ |-------|----------|-------------|
54
+ | `.cursorrules` in agent mode | ⚠ Warning | Agent mode ignores `.cursorrules` — use `.mdc` files |
55
+ | Missing `alwaysApply: true` | ✗ Error | `.mdc` files without this won't load in agent mode |
56
+ | Missing `description` | ⚠ Warning | Cursor uses description to decide when to apply rules |
57
+ | Bad glob syntax | ✗ Error | Comma-separated globs should be YAML arrays |
58
+ | Vague rules | ⚠ Warning | Generic instructions like "follow best practices" have no effect |
59
+ | File too long | ⚠/✗ | Files over 150 lines may not fit in context window |
60
+ | Bad frontmatter | ✗ Error | Malformed YAML frontmatter won't parse |
61
+
62
+ ## CI/CD
63
+
64
+ cursor-lint exits with code 1 when errors are found. Add it to your pipeline:
65
+
66
+ ```yaml
67
+ # GitHub Actions
68
+ - name: Lint Cursor rules
69
+ run: npx cursor-lint
70
+ ```
71
+
72
+ ## Options
73
+
74
+ ```
75
+ cursor-lint [directory] Lint rules in directory (default: current dir)
76
+ cursor-lint --help Show help
77
+ cursor-lint --version Show version
78
+ ```
79
+
80
+ ## Based on Real Testing
81
+
82
+ Every check in cursor-lint comes from [actual experiments](https://dev.to/nedcodes) testing what Cursor does and doesn't follow. Not guesswork — data.
83
+
84
+ ## License
85
+
86
+ MIT
87
+
88
+ ---
89
+
90
+ Made by [nedcodes](https://dev.to/nedcodes)
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "cursor-lint",
3
+ "version": "0.1.0",
4
+ "description": "Lint your Cursor rules — catch common mistakes before they break your workflow",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "cursor-lint": "src/cli.js"
8
+ },
9
+ "keywords": [
10
+ "cursor",
11
+ "cursor-ai",
12
+ "cursorrules",
13
+ "mdc",
14
+ "linter",
15
+ "developer-tools"
16
+ ],
17
+ "author": "nedcodes",
18
+ "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/cursorrulespacks/cursor-lint.git"
22
+ },
23
+ "engines": {
24
+ "node": ">=16"
25
+ },
26
+ "files": [
27
+ "src/"
28
+ ]
29
+ }
package/src/cli.js ADDED
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+
3
+ const path = require('path');
4
+ const { lintProject } = require('./index');
5
+
6
+ const args = process.argv.slice(2);
7
+
8
+ if (args.includes('--help') || args.includes('-h')) {
9
+ console.log(`
10
+ cursor-lint — Lint your Cursor rules
11
+
12
+ Usage:
13
+ cursor-lint [directory] Lint rules in directory (default: current dir)
14
+ cursor-lint --help Show this help
15
+ cursor-lint --version Show version
16
+
17
+ Checks:
18
+ ✗ Missing alwaysApply: true in .mdc files
19
+ ✗ Bad YAML frontmatter / glob syntax
20
+ ⚠ .cursorrules ignored in agent mode
21
+ ⚠ Vague rules ("write clean code", etc.)
22
+ ⚠ Files too long for context window
23
+ `);
24
+ process.exit(0);
25
+ }
26
+
27
+ if (args.includes('--version') || args.includes('-v')) {
28
+ const pkg = require('../package.json');
29
+ console.log(`cursor-lint v${pkg.version}`);
30
+ process.exit(0);
31
+ }
32
+
33
+ const dir = args[0] ? path.resolve(args[0]) : process.cwd();
34
+
35
+ const RED = '\x1b[31m';
36
+ const YELLOW = '\x1b[33m';
37
+ const GREEN = '\x1b[32m';
38
+ const DIM = '\x1b[2m';
39
+ const RESET = '\x1b[0m';
40
+
41
+ async function main() {
42
+ console.log(`\n🔍 cursor-lint v${require('../package.json').version}\n`);
43
+ console.log(`Scanning ${dir}...\n`);
44
+
45
+ const results = await lintProject(dir);
46
+
47
+ let totalErrors = 0;
48
+ let totalWarnings = 0;
49
+ let totalPassed = 0;
50
+
51
+ for (const result of results) {
52
+ const relPath = path.relative(dir, result.file) || result.file;
53
+ console.log(relPath);
54
+
55
+ if (result.issues.length === 0) {
56
+ console.log(` ${GREEN}✓ All checks passed${RESET}`);
57
+ totalPassed++;
58
+ } else {
59
+ for (const issue of result.issues) {
60
+ const icon = issue.severity === 'error' ? `${RED}✗${RESET}` : `${YELLOW}⚠${RESET}`;
61
+ const lineInfo = issue.line ? ` ${DIM}(line ${issue.line})${RESET}` : '';
62
+ console.log(` ${icon} ${issue.message}${lineInfo}`);
63
+ if (issue.hint) {
64
+ console.log(` ${DIM}→ ${issue.hint}${RESET}`);
65
+ }
66
+ }
67
+ const errors = result.issues.filter(i => i.severity === 'error').length;
68
+ const warnings = result.issues.filter(i => i.severity === 'warning').length;
69
+ totalErrors += errors;
70
+ totalWarnings += warnings;
71
+ if (errors === 0 && warnings === 0) totalPassed++;
72
+ }
73
+ console.log();
74
+ }
75
+
76
+ console.log('─'.repeat(50));
77
+ const parts = [];
78
+ if (totalErrors > 0) parts.push(`${RED}${totalErrors} error${totalErrors !== 1 ? 's' : ''}${RESET}`);
79
+ if (totalWarnings > 0) parts.push(`${YELLOW}${totalWarnings} warning${totalWarnings !== 1 ? 's' : ''}${RESET}`);
80
+ if (totalPassed > 0) parts.push(`${GREEN}${totalPassed} passed${RESET}`);
81
+ console.log(parts.join(', ') + '\n');
82
+
83
+ process.exit(totalErrors > 0 ? 1 : 0);
84
+ }
85
+
86
+ main().catch(err => {
87
+ console.error(err);
88
+ process.exit(1);
89
+ });
package/src/index.js ADDED
@@ -0,0 +1,165 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const VAGUE_PATTERNS = [
5
+ 'write clean code',
6
+ 'follow best practices',
7
+ 'be consistent',
8
+ 'write maintainable code',
9
+ 'handle errors properly',
10
+ 'use proper naming',
11
+ 'keep it simple',
12
+ 'write readable code',
13
+ 'follow conventions',
14
+ 'use good patterns',
15
+ 'write efficient code',
16
+ 'be careful',
17
+ 'think before coding',
18
+ 'write good tests',
19
+ 'follow solid principles',
20
+ 'use common sense',
21
+ 'write quality code',
22
+ 'follow the style guide',
23
+ 'be thorough',
24
+ 'write robust code',
25
+ 'use good naming',
26
+ 'be helpful',
27
+ 'use appropriate patterns',
28
+ 'be concise',
29
+ ];
30
+
31
+ const MAX_LINES_WARNING = 150;
32
+ const MAX_LINES_ERROR = 300;
33
+
34
+ function parseFrontmatter(content) {
35
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
36
+ if (!match) return { found: false, data: null, error: null };
37
+
38
+ try {
39
+ const data = {};
40
+ const lines = match[1].split('\n');
41
+ for (const line of lines) {
42
+ const colonIdx = line.indexOf(':');
43
+ if (colonIdx === -1) continue;
44
+ const key = line.slice(0, colonIdx).trim();
45
+ const rawVal = line.slice(colonIdx + 1).trim();
46
+ // Check for bad indentation (key starts with space = likely nested/broken YAML)
47
+ if (line.match(/^\s+\S/) && !line.match(/^\s+-/)) {
48
+ // Indented non-list line where we don't expect it
49
+ const prevLine = lines[lines.indexOf(line) - 1];
50
+ if (prevLine && !prevLine.endsWith(':')) {
51
+ return { found: true, data: null, error: 'Invalid YAML indentation' };
52
+ }
53
+ }
54
+ if (rawVal === 'true') data[key] = true;
55
+ else if (rawVal === 'false') data[key] = false;
56
+ else if (rawVal.startsWith('"') && rawVal.endsWith('"')) data[key] = rawVal.slice(1, -1);
57
+ else data[key] = rawVal;
58
+ }
59
+ return { found: true, data, error: null };
60
+ } catch (e) {
61
+ return { found: true, data: null, error: e.message };
62
+ }
63
+ }
64
+
65
+ async function lintMdcFile(filePath) {
66
+ const content = fs.readFileSync(filePath, 'utf-8');
67
+ const issues = [];
68
+ const lines = content.split('\n');
69
+
70
+ const fm = parseFrontmatter(content);
71
+
72
+ if (!fm.found) {
73
+ issues.push({ severity: 'error', message: 'Missing YAML frontmatter', hint: 'Add --- block with description and alwaysApply: true' });
74
+ } else if (fm.error) {
75
+ issues.push({ severity: 'error', message: `YAML frontmatter error: ${fm.error}`, hint: 'Fix frontmatter indentation/syntax' });
76
+ } else {
77
+ if (!fm.data.alwaysApply) {
78
+ issues.push({ severity: 'error', message: 'Missing alwaysApply: true', hint: 'Add alwaysApply: true to frontmatter for agent mode' });
79
+ }
80
+ if (!fm.data.description) {
81
+ issues.push({ severity: 'warning', message: 'Missing description in frontmatter', hint: 'Add a description so Cursor knows when to apply this rule' });
82
+ }
83
+ if (fm.data.globs && typeof fm.data.globs === 'string' && fm.data.globs.includes(',')) {
84
+ issues.push({ severity: 'error', message: 'Globs should be YAML array, not comma-separated string', hint: 'Use globs:\\n - "*.ts"\\n - "*.tsx"' });
85
+ }
86
+ }
87
+
88
+ // Vague rules
89
+ const contentLower = content.toLowerCase();
90
+ for (const pattern of VAGUE_PATTERNS) {
91
+ const idx = contentLower.indexOf(pattern);
92
+ if (idx !== -1) {
93
+ const lineNum = content.slice(0, idx).split('\n').length;
94
+ issues.push({ severity: 'warning', message: `Vague rule detected: "${pattern}"`, line: lineNum });
95
+ }
96
+ }
97
+
98
+ // File length
99
+ if (lines.length > MAX_LINES_ERROR) {
100
+ issues.push({ severity: 'error', message: `File is ${lines.length} lines long — may exceed context window`, hint: 'Split into multiple .mdc files' });
101
+ } else if (lines.length > MAX_LINES_WARNING) {
102
+ issues.push({ severity: 'warning', message: `File is ${lines.length} lines long — consider splitting`, hint: 'Shorter files are more reliably loaded into context' });
103
+ }
104
+
105
+ return { file: filePath, issues };
106
+ }
107
+
108
+ async function lintCursorrules(filePath) {
109
+ const content = fs.readFileSync(filePath, 'utf-8');
110
+ const issues = [];
111
+
112
+ issues.push({
113
+ severity: 'warning',
114
+ message: '.cursorrules may be ignored in agent mode',
115
+ hint: 'Use .cursor/rules/*.mdc with alwaysApply: true for agent mode compatibility',
116
+ });
117
+
118
+ // Vague rules
119
+ const contentLower = content.toLowerCase();
120
+ for (const pattern of VAGUE_PATTERNS) {
121
+ const idx = contentLower.indexOf(pattern);
122
+ if (idx !== -1) {
123
+ const lineNum = content.slice(0, idx).split('\n').length;
124
+ issues.push({ severity: 'warning', message: `Vague rule detected: "${pattern}"`, line: lineNum });
125
+ }
126
+ }
127
+
128
+ const lines = content.split('\n');
129
+ if (lines.length > MAX_LINES_ERROR) {
130
+ issues.push({ severity: 'error', message: `File is ${lines.length} lines long — may exceed context window` });
131
+ } else if (lines.length > MAX_LINES_WARNING) {
132
+ issues.push({ severity: 'warning', message: `File is ${lines.length} lines long — consider splitting` });
133
+ }
134
+
135
+ return { file: filePath, issues };
136
+ }
137
+
138
+ async function lintProject(dir) {
139
+ const results = [];
140
+
141
+ const cursorrules = path.join(dir, '.cursorrules');
142
+ if (fs.existsSync(cursorrules)) {
143
+ results.push(await lintCursorrules(cursorrules));
144
+ }
145
+
146
+ const rulesDir = path.join(dir, '.cursor', 'rules');
147
+ if (fs.existsSync(rulesDir) && fs.statSync(rulesDir).isDirectory()) {
148
+ for (const entry of fs.readdirSync(rulesDir)) {
149
+ if (entry.endsWith('.mdc')) {
150
+ results.push(await lintMdcFile(path.join(rulesDir, entry)));
151
+ }
152
+ }
153
+ }
154
+
155
+ if (results.length === 0) {
156
+ results.push({
157
+ file: dir,
158
+ issues: [{ severity: 'warning', message: 'No Cursor rules found in this directory' }],
159
+ });
160
+ }
161
+
162
+ return results;
163
+ }
164
+
165
+ module.exports = { lintProject, lintMdcFile, lintCursorrules };