cursor-lint 0.1.1 → 0.2.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 +2 -2
  2. package/src/cli.js +153 -64
  3. package/src/verify.js +304 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cursor-lint",
3
- "version": "0.1.1",
4
- "description": "Lint your Cursor rules catch common mistakes before they break your workflow",
3
+ "version": "0.2.0",
4
+ "description": "Lint your Cursor rules \u2014 catch common mistakes before they break your workflow",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "cursor-lint": "src/cli.js"
package/src/cli.js CHANGED
@@ -2,84 +2,173 @@
2
2
 
3
3
  const path = require('path');
4
4
  const { lintProject } = require('./index');
5
+ const { verifyProject } = require('./verify');
5
6
 
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
- `);
23
- process.exit(0);
24
- }
25
-
26
- if (args.includes('--version') || args.includes('-v')) {
27
- const pkg = require('../package.json');
28
- console.log(`cursor-lint v${pkg.version}`);
29
- process.exit(0);
30
- }
31
-
32
- const dir = args[0] ? path.resolve(args[0]) : process.cwd();
7
+ const VERSION = '0.2.0';
33
8
 
34
9
  const RED = '\x1b[31m';
35
10
  const YELLOW = '\x1b[33m';
36
11
  const GREEN = '\x1b[32m';
12
+ const CYAN = '\x1b[36m';
37
13
  const DIM = '\x1b[2m';
38
14
  const RESET = '\x1b[0m';
39
15
 
16
+ function showHelp() {
17
+ console.log(`
18
+ ${CYAN}cursor-lint${RESET} v${VERSION}
19
+
20
+ Lint your Cursor rules and verify code compliance.
21
+
22
+ ${YELLOW}Usage:${RESET}
23
+ npx cursor-lint [options]
24
+
25
+ ${YELLOW}Options:${RESET}
26
+ --help, -h Show this help message
27
+ --version, -v Show version number
28
+ --verify Check if code follows rules with verify: blocks
29
+
30
+ ${YELLOW}What it checks (default):${RESET}
31
+ • .cursorrules files (warns about agent mode compatibility)
32
+ • .cursor/rules/*.mdc files (frontmatter, alwaysApply, etc.)
33
+ • Vague rules that won't change AI behavior
34
+ • YAML syntax errors
35
+
36
+ ${YELLOW}What --verify checks:${RESET}
37
+ • Scans code files matching rule globs
38
+ • Checks for required patterns (pattern:, required:)
39
+ • Catches forbidden patterns (antipattern:, forbidden:)
40
+ • Reports violations with line numbers
41
+
42
+ ${YELLOW}verify: block syntax in .mdc frontmatter:${RESET}
43
+ ---
44
+ globs: ["*.ts", "*.tsx"]
45
+ verify:
46
+ - pattern: "^import.*from '@/"
47
+ message: "Use @/ alias for imports"
48
+ - antipattern: "console\\\\.log"
49
+ message: "Remove console.log"
50
+ - required: "use strict"
51
+ message: "Missing use strict"
52
+ - forbidden: "TODO"
53
+ message: "Resolve TODOs before commit"
54
+ ---
55
+
56
+ ${YELLOW}Examples:${RESET}
57
+ npx cursor-lint # Lint rule files
58
+ npx cursor-lint --verify # Check code against rules
59
+
60
+ ${YELLOW}More info:${RESET}
61
+ https://github.com/cursorrulespacks/cursor-lint
62
+ `);
63
+ }
64
+
40
65
  async function main() {
41
- console.log(`\n🔍 cursor-lint v${require('../package.json').version}\n`);
42
- console.log(`Scanning ${dir}...\n`);
43
-
44
- const results = await lintProject(dir);
45
-
46
- let totalErrors = 0;
47
- let totalWarnings = 0;
48
- let totalPassed = 0;
49
-
50
- for (const result of results) {
51
- const relPath = path.relative(dir, result.file) || result.file;
52
- console.log(relPath);
53
-
54
- if (result.issues.length === 0) {
55
- console.log(` ${GREEN}✓ All checks passed${RESET}`);
56
- totalPassed++;
57
- } else {
58
- for (const issue of result.issues) {
59
- const icon = issue.severity === 'error' ? `${RED}✗${RESET}` : `${YELLOW}⚠${RESET}`;
60
- const lineInfo = issue.line ? ` ${DIM}(line ${issue.line})${RESET}` : '';
61
- console.log(` ${icon} ${issue.message}${lineInfo}`);
62
- if (issue.hint) {
63
- console.log(` ${DIM} ${issue.hint}${RESET}`);
66
+ const args = process.argv.slice(2);
67
+
68
+ if (args.includes('--help') || args.includes('-h')) {
69
+ showHelp();
70
+ process.exit(0);
71
+ }
72
+
73
+ if (args.includes('--version') || args.includes('-v')) {
74
+ console.log(VERSION);
75
+ process.exit(0);
76
+ }
77
+
78
+ const cwd = process.cwd();
79
+ const isVerify = args.includes('--verify');
80
+
81
+ if (isVerify) {
82
+ console.log(`\n🔍 cursor-lint v${VERSION} --verify\n`);
83
+ console.log(`Scanning ${cwd} for rule violations...\n`);
84
+
85
+ const results = await verifyProject(cwd);
86
+
87
+ if (results.stats.rulesWithVerify === 0) {
88
+ console.log(`${YELLOW}No rules with verify: blocks found.${RESET}`);
89
+ console.log(`${DIM}Add verify: blocks to your .mdc frontmatter to check code compliance.${RESET}`);
90
+ console.log(`${DIM}Run cursor-lint --help for syntax.${RESET}\n`);
91
+ process.exit(0);
92
+ }
93
+
94
+ console.log(`Found ${results.stats.rulesWithVerify} rule(s) with verify blocks`);
95
+ console.log(`Checked ${results.stats.filesChecked} file(s)\n`);
96
+
97
+ if (results.violations.length === 0) {
98
+ console.log(`${GREEN}✓ No violations found${RESET}\n`);
99
+ process.exit(0);
100
+ }
101
+
102
+ // Group violations by file
103
+ const byFile = {};
104
+ for (const v of results.violations) {
105
+ if (!byFile[v.file]) byFile[v.file] = [];
106
+ byFile[v.file].push(v);
107
+ }
108
+
109
+ for (const [file, violations] of Object.entries(byFile)) {
110
+ console.log(`${file}`);
111
+ for (const v of violations) {
112
+ const lineInfo = v.line ? ` ${DIM}(line ${v.line})${RESET}` : '';
113
+ console.log(` ${RED}✗${RESET} ${v.message}${lineInfo}`);
114
+ if (v.match) {
115
+ console.log(` ${DIM}→ ${v.match}${RESET}`);
64
116
  }
65
117
  }
66
- const errors = result.issues.filter(i => i.severity === 'error').length;
67
- const warnings = result.issues.filter(i => i.severity === 'warning').length;
68
- totalErrors += errors;
69
- totalWarnings += warnings;
70
- if (errors === 0 && warnings === 0) totalPassed++;
118
+ console.log();
71
119
  }
72
- console.log();
73
- }
74
120
 
75
- console.log('─'.repeat(50));
76
- const parts = [];
77
- if (totalErrors > 0) parts.push(`${RED}${totalErrors} error${totalErrors !== 1 ? 's' : ''}${RESET}`);
78
- if (totalWarnings > 0) parts.push(`${YELLOW}${totalWarnings} warning${totalWarnings !== 1 ? 's' : ''}${RESET}`);
79
- if (totalPassed > 0) parts.push(`${GREEN}${totalPassed} passed${RESET}`);
80
- console.log(parts.join(', ') + '\n');
121
+ console.log('─'.repeat(50));
122
+ console.log(`${RED}${results.stats.totalViolations} violation(s)${RESET} in ${results.stats.filesWithViolations} file(s)\n`);
123
+ process.exit(1);
124
+
125
+ } else {
126
+ // Original lint mode
127
+ const dir = args[0] ? path.resolve(args[0]) : cwd;
128
+
129
+ console.log(`\n🔍 cursor-lint v${VERSION}\n`);
130
+ console.log(`Scanning ${dir}...\n`);
131
+
132
+ const results = await lintProject(dir);
133
+
134
+ let totalErrors = 0;
135
+ let totalWarnings = 0;
136
+ let totalPassed = 0;
137
+
138
+ for (const result of results) {
139
+ const relPath = path.relative(dir, result.file) || result.file;
140
+ console.log(relPath);
141
+
142
+ if (result.issues.length === 0) {
143
+ console.log(` ${GREEN}✓ All checks passed${RESET}`);
144
+ totalPassed++;
145
+ } else {
146
+ for (const issue of result.issues) {
147
+ const icon = issue.severity === 'error' ? `${RED}✗${RESET}` : `${YELLOW}⚠${RESET}`;
148
+ const lineInfo = issue.line ? ` ${DIM}(line ${issue.line})${RESET}` : '';
149
+ console.log(` ${icon} ${issue.message}${lineInfo}`);
150
+ if (issue.hint) {
151
+ console.log(` ${DIM}→ ${issue.hint}${RESET}`);
152
+ }
153
+ }
154
+ const errors = result.issues.filter(i => i.severity === 'error').length;
155
+ const warnings = result.issues.filter(i => i.severity === 'warning').length;
156
+ totalErrors += errors;
157
+ totalWarnings += warnings;
158
+ if (errors === 0 && warnings === 0) totalPassed++;
159
+ }
160
+ console.log();
161
+ }
162
+
163
+ console.log('─'.repeat(50));
164
+ const parts = [];
165
+ if (totalErrors > 0) parts.push(`${RED}${totalErrors} error${totalErrors !== 1 ? 's' : ''}${RESET}`);
166
+ if (totalWarnings > 0) parts.push(`${YELLOW}${totalWarnings} warning${totalWarnings !== 1 ? 's' : ''}${RESET}`);
167
+ if (totalPassed > 0) parts.push(`${GREEN}${totalPassed} passed${RESET}`);
168
+ console.log(parts.join(', ') + '\n');
81
169
 
82
- process.exit(totalErrors > 0 ? 1 : 0);
170
+ process.exit(totalErrors > 0 ? 1 : 0);
171
+ }
83
172
  }
84
173
 
85
174
  main().catch(err => {
package/src/verify.js ADDED
@@ -0,0 +1,304 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Verify codebase files against rules with verify: blocks
6
+ * Zero dependencies — uses simple YAML parsing and manual glob
7
+ */
8
+ async function verifyProject(projectPath) {
9
+ const results = {
10
+ rules: [],
11
+ violations: [],
12
+ stats: {
13
+ rulesWithVerify: 0,
14
+ filesChecked: 0,
15
+ filesWithViolations: 0,
16
+ totalViolations: 0
17
+ }
18
+ };
19
+
20
+ const mdcDir = path.join(projectPath, '.cursor', 'rules');
21
+ if (!fs.existsSync(mdcDir)) {
22
+ return results;
23
+ }
24
+
25
+ const mdcFiles = fs.readdirSync(mdcDir).filter(f => f.endsWith('.mdc'));
26
+
27
+ for (const file of mdcFiles) {
28
+ const fullPath = path.join(mdcDir, file);
29
+ const content = fs.readFileSync(fullPath, 'utf-8');
30
+ const frontmatter = parseFrontmatter(content);
31
+
32
+ if (!frontmatter.data || !frontmatter.data.verify) {
33
+ continue;
34
+ }
35
+
36
+ results.rules.push({
37
+ file,
38
+ globs: frontmatter.data.globs || ['**/*'],
39
+ verify: frontmatter.data.verify
40
+ });
41
+ results.stats.rulesWithVerify++;
42
+ }
43
+
44
+ if (results.rules.length === 0) {
45
+ return results;
46
+ }
47
+
48
+ for (const rule of results.rules) {
49
+ const globs = Array.isArray(rule.globs) ? rule.globs : [rule.globs];
50
+ const matchingFiles = findFiles(projectPath, globs);
51
+
52
+ for (const file of matchingFiles) {
53
+ results.stats.filesChecked++;
54
+ const fullPath = path.join(projectPath, file);
55
+
56
+ try {
57
+ const stats = fs.statSync(fullPath);
58
+ if (stats.size > 1024 * 1024) continue;
59
+ } catch (e) {
60
+ continue;
61
+ }
62
+
63
+ let content;
64
+ try {
65
+ content = fs.readFileSync(fullPath, 'utf-8');
66
+ } catch (e) {
67
+ continue;
68
+ }
69
+
70
+ const fileViolations = checkFile(file, content, rule.verify, rule.file);
71
+
72
+ if (fileViolations.length > 0) {
73
+ results.stats.filesWithViolations++;
74
+ results.stats.totalViolations += fileViolations.length;
75
+ results.violations.push(...fileViolations);
76
+ }
77
+ }
78
+ }
79
+
80
+ return results;
81
+ }
82
+
83
+ function checkFile(filePath, content, verifyBlocks, ruleFile) {
84
+ const violations = [];
85
+
86
+ for (const block of verifyBlocks) {
87
+ if (block.pattern) {
88
+ try {
89
+ const regex = new RegExp(block.pattern, 'm');
90
+ if (!regex.test(content)) {
91
+ violations.push({
92
+ file: filePath,
93
+ ruleFile,
94
+ type: 'missing-pattern',
95
+ message: block.message || `Missing required pattern: ${block.pattern}`,
96
+ pattern: block.pattern
97
+ });
98
+ }
99
+ } catch (e) {}
100
+ }
101
+
102
+ if (block.antipattern) {
103
+ try {
104
+ const regex = new RegExp(block.antipattern, 'gm');
105
+ let match;
106
+ while ((match = regex.exec(content)) !== null) {
107
+ const lineNum = content.substring(0, match.index).split('\n').length;
108
+ violations.push({
109
+ file: filePath,
110
+ ruleFile,
111
+ type: 'antipattern',
112
+ message: block.message || `Forbidden pattern found: ${block.antipattern}`,
113
+ pattern: block.antipattern,
114
+ line: lineNum,
115
+ match: match[0].substring(0, 50) + (match[0].length > 50 ? '...' : '')
116
+ });
117
+ }
118
+ } catch (e) {}
119
+ }
120
+
121
+ if (block.required) {
122
+ if (!content.includes(block.required)) {
123
+ violations.push({
124
+ file: filePath,
125
+ ruleFile,
126
+ type: 'missing-required',
127
+ message: block.message || `Missing required string: "${block.required}"`,
128
+ required: block.required
129
+ });
130
+ }
131
+ }
132
+
133
+ if (block.forbidden) {
134
+ const index = content.indexOf(block.forbidden);
135
+ if (index !== -1) {
136
+ const lineNum = content.substring(0, index).split('\n').length;
137
+ violations.push({
138
+ file: filePath,
139
+ ruleFile,
140
+ type: 'forbidden',
141
+ message: block.message || `Forbidden string found: "${block.forbidden}"`,
142
+ forbidden: block.forbidden,
143
+ line: lineNum
144
+ });
145
+ }
146
+ }
147
+ }
148
+
149
+ return violations;
150
+ }
151
+
152
+ /**
153
+ * Simple frontmatter parser that handles verify blocks
154
+ */
155
+ function parseFrontmatter(content) {
156
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
157
+ if (!match) {
158
+ return { data: null };
159
+ }
160
+
161
+ try {
162
+ const data = parseSimpleYaml(match[1]);
163
+ return { data };
164
+ } catch (err) {
165
+ return { error: err.message };
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Minimal YAML parser for frontmatter with verify blocks
171
+ */
172
+ function parseSimpleYaml(text) {
173
+ const data = {};
174
+ const lines = text.split('\n');
175
+ let currentKey = null;
176
+ let currentList = null;
177
+ let currentItem = null;
178
+
179
+ for (let i = 0; i < lines.length; i++) {
180
+ const line = lines[i];
181
+
182
+ // Top-level key: value
183
+ const kvMatch = line.match(/^(\w+):\s*(.+)$/);
184
+ if (kvMatch) {
185
+ currentKey = kvMatch[1];
186
+ currentList = null;
187
+ currentItem = null;
188
+ let val = kvMatch[2].trim();
189
+ // Handle arrays like ["*.ts", "*.tsx"]
190
+ if (val.startsWith('[') && val.endsWith(']')) {
191
+ data[currentKey] = val.slice(1, -1).split(',').map(s => s.trim().replace(/^["']|["']$/g, ''));
192
+ } else if (val === 'true') {
193
+ data[currentKey] = true;
194
+ } else if (val === 'false') {
195
+ data[currentKey] = false;
196
+ } else {
197
+ data[currentKey] = unquote(val);
198
+ }
199
+ continue;
200
+ }
201
+
202
+ // Top-level key with no value (starts a block)
203
+ const blockMatch = line.match(/^(\w+):$/);
204
+ if (blockMatch) {
205
+ currentKey = blockMatch[1];
206
+ currentList = [];
207
+ currentItem = null;
208
+ data[currentKey] = currentList;
209
+ continue;
210
+ }
211
+
212
+ // List item start
213
+ if (currentList !== null && line.match(/^\s+-\s+\w+:/)) {
214
+ const itemMatch = line.match(/^\s+-\s+(\w+):\s*(.+)$/);
215
+ if (itemMatch) {
216
+ currentItem = {};
217
+ let val = unquote(itemMatch[2].trim());
218
+ currentItem[itemMatch[1]] = val;
219
+ currentList.push(currentItem);
220
+ }
221
+ continue;
222
+ }
223
+
224
+ // Continuation of list item
225
+ if (currentItem && line.match(/^\s+\w+:/)) {
226
+ const contMatch = line.match(/^\s+(\w+):\s*(.+)$/);
227
+ if (contMatch) {
228
+ let val = unquote(contMatch[2].trim());
229
+ currentItem[contMatch[1]] = val;
230
+ }
231
+ continue;
232
+ }
233
+ }
234
+
235
+ return data;
236
+ }
237
+
238
+ /**
239
+ * Remove quotes and handle escape sequences
240
+ */
241
+ function unquote(val) {
242
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
243
+ val = val.slice(1, -1);
244
+ // Handle YAML escape sequences in double-quoted strings
245
+ val = val.replace(/\\\\/g, '\\');
246
+ }
247
+ return val;
248
+ }
249
+
250
+ /**
251
+ * Simple file finder matching glob patterns
252
+ */
253
+ function findFiles(baseDir, patterns) {
254
+ const files = [];
255
+ const ignore = ['node_modules', '.git', '.cursor'];
256
+
257
+ function walk(dir, rel) {
258
+ let entries;
259
+ try {
260
+ entries = fs.readdirSync(dir, { withFileTypes: true });
261
+ } catch (e) {
262
+ return;
263
+ }
264
+
265
+ for (const entry of entries) {
266
+ if (ignore.includes(entry.name)) continue;
267
+
268
+ const fullPath = path.join(dir, entry.name);
269
+ const relPath = rel ? path.join(rel, entry.name) : entry.name;
270
+
271
+ if (entry.isDirectory()) {
272
+ walk(fullPath, relPath);
273
+ } else if (entry.isFile()) {
274
+ for (const pattern of patterns) {
275
+ if (matchGlob(relPath, pattern)) {
276
+ files.push(relPath);
277
+ break;
278
+ }
279
+ }
280
+ }
281
+ }
282
+ }
283
+
284
+ walk(baseDir, '');
285
+ return files;
286
+ }
287
+
288
+ /**
289
+ * Simple glob matching (supports *, **, and extensions like *.ts)
290
+ */
291
+ function matchGlob(filePath, pattern) {
292
+ // Simple extension match: *.ts, *.tsx, etc.
293
+ if (pattern.startsWith('*.')) {
294
+ return filePath.endsWith(pattern.slice(1));
295
+ }
296
+ // **/*.ext
297
+ if (pattern.startsWith('**/')) {
298
+ return matchGlob(filePath, pattern.slice(3));
299
+ }
300
+ // Direct match
301
+ return filePath === pattern;
302
+ }
303
+
304
+ module.exports = { verifyProject, checkFile };