cursor-lint 0.1.1 → 0.3.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 +192 -64
- package/src/init.js +227 -0
- package/src/verify.js +304 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -2,84 +2,212 @@
|
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { lintProject } = require('./index');
|
|
5
|
+
const { verifyProject } = require('./verify');
|
|
6
|
+
const { initProject } = require('./init');
|
|
5
7
|
|
|
6
|
-
const
|
|
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();
|
|
8
|
+
const VERSION = '0.3.0';
|
|
33
9
|
|
|
34
10
|
const RED = '\x1b[31m';
|
|
35
11
|
const YELLOW = '\x1b[33m';
|
|
36
12
|
const GREEN = '\x1b[32m';
|
|
13
|
+
const CYAN = '\x1b[36m';
|
|
37
14
|
const DIM = '\x1b[2m';
|
|
38
15
|
const RESET = '\x1b[0m';
|
|
39
16
|
|
|
17
|
+
function showHelp() {
|
|
18
|
+
console.log(`
|
|
19
|
+
${CYAN}cursor-lint${RESET} v${VERSION}
|
|
20
|
+
|
|
21
|
+
Lint your Cursor rules and verify code compliance.
|
|
22
|
+
|
|
23
|
+
${YELLOW}Usage:${RESET}
|
|
24
|
+
npx cursor-lint [options]
|
|
25
|
+
|
|
26
|
+
${YELLOW}Options:${RESET}
|
|
27
|
+
--help, -h Show this help message
|
|
28
|
+
--version, -v Show version number
|
|
29
|
+
--verify Check if code follows rules with verify: blocks
|
|
30
|
+
--init Generate starter .mdc rules (auto-detects your stack)
|
|
31
|
+
|
|
32
|
+
${YELLOW}What it checks (default):${RESET}
|
|
33
|
+
• .cursorrules files (warns about agent mode compatibility)
|
|
34
|
+
• .cursor/rules/*.mdc files (frontmatter, alwaysApply, etc.)
|
|
35
|
+
• Vague rules that won't change AI behavior
|
|
36
|
+
• YAML syntax errors
|
|
37
|
+
|
|
38
|
+
${YELLOW}What --verify checks:${RESET}
|
|
39
|
+
• Scans code files matching rule globs
|
|
40
|
+
• Checks for required patterns (pattern:, required:)
|
|
41
|
+
• Catches forbidden patterns (antipattern:, forbidden:)
|
|
42
|
+
• Reports violations with line numbers
|
|
43
|
+
|
|
44
|
+
${YELLOW}verify: block syntax in .mdc frontmatter:${RESET}
|
|
45
|
+
---
|
|
46
|
+
globs: ["*.ts", "*.tsx"]
|
|
47
|
+
verify:
|
|
48
|
+
- pattern: "^import.*from '@/"
|
|
49
|
+
message: "Use @/ alias for imports"
|
|
50
|
+
- antipattern: "console\\\\.log"
|
|
51
|
+
message: "Remove console.log"
|
|
52
|
+
- required: "use strict"
|
|
53
|
+
message: "Missing use strict"
|
|
54
|
+
- forbidden: "TODO"
|
|
55
|
+
message: "Resolve TODOs before commit"
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
${YELLOW}Examples:${RESET}
|
|
59
|
+
npx cursor-lint # Lint rule files
|
|
60
|
+
npx cursor-lint --verify # Check code against rules
|
|
61
|
+
npx cursor-lint --init # Generate starter rules for your project
|
|
62
|
+
|
|
63
|
+
${YELLOW}More info:${RESET}
|
|
64
|
+
https://github.com/cursorrulespacks/cursor-lint
|
|
65
|
+
`);
|
|
66
|
+
}
|
|
67
|
+
|
|
40
68
|
async function main() {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
69
|
+
const args = process.argv.slice(2);
|
|
70
|
+
|
|
71
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
72
|
+
showHelp();
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
77
|
+
console.log(VERSION);
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const cwd = process.cwd();
|
|
82
|
+
const isVerify = args.includes('--verify');
|
|
83
|
+
const isInit = args.includes('--init');
|
|
84
|
+
|
|
85
|
+
if (isInit) {
|
|
86
|
+
console.log(`\n🔍 cursor-lint v${VERSION} --init\n`);
|
|
87
|
+
console.log(`Detecting stack in ${cwd}...\n`);
|
|
88
|
+
|
|
89
|
+
const results = await initProject(cwd);
|
|
90
|
+
|
|
91
|
+
const stacks = Object.entries(results.detected)
|
|
92
|
+
.filter(([_, v]) => v)
|
|
93
|
+
.map(([k]) => k.charAt(0).toUpperCase() + k.slice(1));
|
|
94
|
+
|
|
95
|
+
if (stacks.length > 0) {
|
|
96
|
+
console.log(`Detected: ${stacks.join(', ')}\n`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (results.created.length > 0) {
|
|
100
|
+
console.log(`${GREEN}Created:${RESET}`);
|
|
101
|
+
for (const f of results.created) {
|
|
102
|
+
console.log(` ${GREEN}✓${RESET} .cursor/rules/${f}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (results.skipped.length > 0) {
|
|
107
|
+
console.log(`\n${YELLOW}Skipped (already exist):${RESET}`);
|
|
108
|
+
for (const f of results.skipped) {
|
|
109
|
+
console.log(` ${YELLOW}⚠${RESET} .cursor/rules/${f}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (results.created.length > 0) {
|
|
114
|
+
console.log(`\n${DIM}Run cursor-lint to check these rules${RESET}`);
|
|
115
|
+
console.log(`${DIM}Run cursor-lint --verify to check code against them${RESET}\n`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
process.exit(0);
|
|
119
|
+
|
|
120
|
+
} else if (isVerify) {
|
|
121
|
+
console.log(`\n🔍 cursor-lint v${VERSION} --verify\n`);
|
|
122
|
+
console.log(`Scanning ${cwd} for rule violations...\n`);
|
|
123
|
+
|
|
124
|
+
const results = await verifyProject(cwd);
|
|
125
|
+
|
|
126
|
+
if (results.stats.rulesWithVerify === 0) {
|
|
127
|
+
console.log(`${YELLOW}No rules with verify: blocks found.${RESET}`);
|
|
128
|
+
console.log(`${DIM}Add verify: blocks to your .mdc frontmatter to check code compliance.${RESET}`);
|
|
129
|
+
console.log(`${DIM}Run cursor-lint --help for syntax.${RESET}\n`);
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log(`Found ${results.stats.rulesWithVerify} rule(s) with verify blocks`);
|
|
134
|
+
console.log(`Checked ${results.stats.filesChecked} file(s)\n`);
|
|
135
|
+
|
|
136
|
+
if (results.violations.length === 0) {
|
|
137
|
+
console.log(`${GREEN}✓ No violations found${RESET}\n`);
|
|
138
|
+
process.exit(0);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Group violations by file
|
|
142
|
+
const byFile = {};
|
|
143
|
+
for (const v of results.violations) {
|
|
144
|
+
if (!byFile[v.file]) byFile[v.file] = [];
|
|
145
|
+
byFile[v.file].push(v);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const [file, violations] of Object.entries(byFile)) {
|
|
149
|
+
console.log(`${file}`);
|
|
150
|
+
for (const v of violations) {
|
|
151
|
+
const lineInfo = v.line ? ` ${DIM}(line ${v.line})${RESET}` : '';
|
|
152
|
+
console.log(` ${RED}✗${RESET} ${v.message}${lineInfo}`);
|
|
153
|
+
if (v.match) {
|
|
154
|
+
console.log(` ${DIM}→ ${v.match}${RESET}`);
|
|
64
155
|
}
|
|
65
156
|
}
|
|
66
|
-
|
|
67
|
-
const warnings = result.issues.filter(i => i.severity === 'warning').length;
|
|
68
|
-
totalErrors += errors;
|
|
69
|
-
totalWarnings += warnings;
|
|
70
|
-
if (errors === 0 && warnings === 0) totalPassed++;
|
|
157
|
+
console.log();
|
|
71
158
|
}
|
|
72
|
-
console.log();
|
|
73
|
-
}
|
|
74
159
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
160
|
+
console.log('─'.repeat(50));
|
|
161
|
+
console.log(`${RED}${results.stats.totalViolations} violation(s)${RESET} in ${results.stats.filesWithViolations} file(s)\n`);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
|
|
164
|
+
} else {
|
|
165
|
+
// Original lint mode
|
|
166
|
+
const dir = args[0] ? path.resolve(args[0]) : cwd;
|
|
167
|
+
|
|
168
|
+
console.log(`\n🔍 cursor-lint v${VERSION}\n`);
|
|
169
|
+
console.log(`Scanning ${dir}...\n`);
|
|
170
|
+
|
|
171
|
+
const results = await lintProject(dir);
|
|
172
|
+
|
|
173
|
+
let totalErrors = 0;
|
|
174
|
+
let totalWarnings = 0;
|
|
175
|
+
let totalPassed = 0;
|
|
176
|
+
|
|
177
|
+
for (const result of results) {
|
|
178
|
+
const relPath = path.relative(dir, result.file) || result.file;
|
|
179
|
+
console.log(relPath);
|
|
180
|
+
|
|
181
|
+
if (result.issues.length === 0) {
|
|
182
|
+
console.log(` ${GREEN}✓ All checks passed${RESET}`);
|
|
183
|
+
totalPassed++;
|
|
184
|
+
} else {
|
|
185
|
+
for (const issue of result.issues) {
|
|
186
|
+
const icon = issue.severity === 'error' ? `${RED}✗${RESET}` : `${YELLOW}⚠${RESET}`;
|
|
187
|
+
const lineInfo = issue.line ? ` ${DIM}(line ${issue.line})${RESET}` : '';
|
|
188
|
+
console.log(` ${icon} ${issue.message}${lineInfo}`);
|
|
189
|
+
if (issue.hint) {
|
|
190
|
+
console.log(` ${DIM}→ ${issue.hint}${RESET}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const errors = result.issues.filter(i => i.severity === 'error').length;
|
|
194
|
+
const warnings = result.issues.filter(i => i.severity === 'warning').length;
|
|
195
|
+
totalErrors += errors;
|
|
196
|
+
totalWarnings += warnings;
|
|
197
|
+
if (errors === 0 && warnings === 0) totalPassed++;
|
|
198
|
+
}
|
|
199
|
+
console.log();
|
|
200
|
+
}
|
|
81
201
|
|
|
82
|
-
|
|
202
|
+
console.log('─'.repeat(50));
|
|
203
|
+
const parts = [];
|
|
204
|
+
if (totalErrors > 0) parts.push(`${RED}${totalErrors} error${totalErrors !== 1 ? 's' : ''}${RESET}`);
|
|
205
|
+
if (totalWarnings > 0) parts.push(`${YELLOW}${totalWarnings} warning${totalWarnings !== 1 ? 's' : ''}${RESET}`);
|
|
206
|
+
if (totalPassed > 0) parts.push(`${GREEN}${totalPassed} passed${RESET}`);
|
|
207
|
+
console.log(parts.join(', ') + '\n');
|
|
208
|
+
|
|
209
|
+
process.exit(totalErrors > 0 ? 1 : 0);
|
|
210
|
+
}
|
|
83
211
|
}
|
|
84
212
|
|
|
85
213
|
main().catch(err => {
|
package/src/init.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
async function initProject(projectPath) {
|
|
5
|
+
const detected = detectStack(projectPath);
|
|
6
|
+
const created = [];
|
|
7
|
+
const skipped = [];
|
|
8
|
+
|
|
9
|
+
const rulesDir = path.join(projectPath, '.cursor', 'rules');
|
|
10
|
+
|
|
11
|
+
if (!fs.existsSync(rulesDir)) {
|
|
12
|
+
fs.mkdirSync(rulesDir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const generalResult = writeRule(rulesDir, 'general.mdc', generateGeneral());
|
|
16
|
+
if (generalResult.created) created.push(generalResult.file);
|
|
17
|
+
else skipped.push(generalResult.file);
|
|
18
|
+
|
|
19
|
+
if (detected.typescript) {
|
|
20
|
+
const result = writeRule(rulesDir, 'typescript.mdc', generateTypeScript());
|
|
21
|
+
if (result.created) created.push(result.file);
|
|
22
|
+
else skipped.push(result.file);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (detected.react && !detected.nextjs) {
|
|
26
|
+
const result = writeRule(rulesDir, 'react.mdc', generateReact());
|
|
27
|
+
if (result.created) created.push(result.file);
|
|
28
|
+
else skipped.push(result.file);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (detected.nextjs) {
|
|
32
|
+
const result = writeRule(rulesDir, 'nextjs.mdc', generateNextJs());
|
|
33
|
+
if (result.created) created.push(result.file);
|
|
34
|
+
else skipped.push(result.file);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (detected.express) {
|
|
38
|
+
const result = writeRule(rulesDir, 'express.mdc', generateExpress());
|
|
39
|
+
if (result.created) created.push(result.file);
|
|
40
|
+
else skipped.push(result.file);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (detected.python) {
|
|
44
|
+
const result = writeRule(rulesDir, 'python.mdc', generatePython());
|
|
45
|
+
if (result.created) created.push(result.file);
|
|
46
|
+
else skipped.push(result.file);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { created, skipped, detected };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function detectStack(projectPath) {
|
|
53
|
+
const detected = {
|
|
54
|
+
typescript: false,
|
|
55
|
+
react: false,
|
|
56
|
+
nextjs: false,
|
|
57
|
+
express: false,
|
|
58
|
+
python: false,
|
|
59
|
+
node: false
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (fs.existsSync(path.join(projectPath, 'tsconfig.json'))) {
|
|
63
|
+
detected.typescript = true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const pkgPath = path.join(projectPath, 'package.json');
|
|
67
|
+
if (fs.existsSync(pkgPath)) {
|
|
68
|
+
detected.node = true;
|
|
69
|
+
try {
|
|
70
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
71
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
72
|
+
|
|
73
|
+
if (allDeps.react || allDeps['react-dom']) detected.react = true;
|
|
74
|
+
if (allDeps.next) { detected.nextjs = true; detected.react = true; }
|
|
75
|
+
if (allDeps.express) detected.express = true;
|
|
76
|
+
if (allDeps.typescript || allDeps['@types/node']) detected.typescript = true;
|
|
77
|
+
} catch (e) {}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const files = fs.readdirSync(projectPath);
|
|
82
|
+
if (files.some(f => f.endsWith('.py')) ||
|
|
83
|
+
fs.existsSync(path.join(projectPath, 'requirements.txt')) ||
|
|
84
|
+
fs.existsSync(path.join(projectPath, 'pyproject.toml'))) {
|
|
85
|
+
detected.python = true;
|
|
86
|
+
}
|
|
87
|
+
} catch (e) {}
|
|
88
|
+
|
|
89
|
+
return detected;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function writeRule(rulesDir, filename, content) {
|
|
93
|
+
const filePath = path.join(rulesDir, filename);
|
|
94
|
+
if (fs.existsSync(filePath)) return { file: filename, created: false };
|
|
95
|
+
fs.writeFileSync(filePath, content);
|
|
96
|
+
return { file: filename, created: true };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function generateGeneral() {
|
|
100
|
+
return `---
|
|
101
|
+
description: General code quality rules
|
|
102
|
+
alwaysApply: true
|
|
103
|
+
globs: ["*"]
|
|
104
|
+
verify:
|
|
105
|
+
- antipattern: "TODO"
|
|
106
|
+
message: "Resolve TODO comments before committing"
|
|
107
|
+
- antipattern: "FIXME"
|
|
108
|
+
message: "Resolve FIXME comments before committing"
|
|
109
|
+
- antipattern: "console\\\\.log"
|
|
110
|
+
message: "Remove console.log statements"
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
# General Guidelines
|
|
114
|
+
|
|
115
|
+
- Write clear, self-documenting code
|
|
116
|
+
- Use meaningful variable and function names
|
|
117
|
+
- Keep functions small and focused
|
|
118
|
+
- Remove all TODOs and FIXMEs before committing
|
|
119
|
+
- No console.log in production code
|
|
120
|
+
`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function generateTypeScript() {
|
|
124
|
+
return `---
|
|
125
|
+
description: TypeScript best practices
|
|
126
|
+
alwaysApply: true
|
|
127
|
+
globs: ["*.ts", "*.tsx"]
|
|
128
|
+
verify:
|
|
129
|
+
- antipattern: ": any"
|
|
130
|
+
message: "Avoid using 'any' type - use proper typing"
|
|
131
|
+
- antipattern: "@ts-ignore"
|
|
132
|
+
message: "Remove @ts-ignore - fix the type error instead"
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
# TypeScript Rules
|
|
136
|
+
|
|
137
|
+
- Use strict TypeScript configuration
|
|
138
|
+
- Avoid \`any\` type - use \`unknown\` if type is truly unknown
|
|
139
|
+
- Use type inference where possible, explicit types where helpful
|
|
140
|
+
- Prefer interfaces for object shapes, types for unions/intersections
|
|
141
|
+
`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function generateReact() {
|
|
145
|
+
return `---
|
|
146
|
+
description: React best practices
|
|
147
|
+
alwaysApply: true
|
|
148
|
+
globs: ["*.tsx", "*.jsx"]
|
|
149
|
+
verify:
|
|
150
|
+
- antipattern: "dangerouslySetInnerHTML"
|
|
151
|
+
message: "Avoid dangerouslySetInnerHTML - use proper sanitization if needed"
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
# React Rules
|
|
155
|
+
|
|
156
|
+
- Use functional components with hooks
|
|
157
|
+
- Before writing a useEffect, ask: can this be computed during render?
|
|
158
|
+
- Keep components small and focused
|
|
159
|
+
- Use proper key props in lists (never use array index as key for dynamic lists)
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function generateNextJs() {
|
|
164
|
+
return `---
|
|
165
|
+
description: Next.js App Router best practices
|
|
166
|
+
alwaysApply: true
|
|
167
|
+
globs: ["*.ts", "*.tsx"]
|
|
168
|
+
verify:
|
|
169
|
+
- antipattern: "getServerSideProps"
|
|
170
|
+
message: "Use App Router patterns instead of getServerSideProps"
|
|
171
|
+
- antipattern: "getStaticProps"
|
|
172
|
+
message: "Use App Router patterns instead of getStaticProps"
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
# Next.js Rules
|
|
176
|
+
|
|
177
|
+
- Use App Router (app directory), not Pages Router
|
|
178
|
+
- Mark components as 'use client' only when they need client-side interactivity
|
|
179
|
+
- Default to Server Components
|
|
180
|
+
- Use Server Actions for mutations instead of API routes
|
|
181
|
+
- Use the @/ path alias for imports
|
|
182
|
+
`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function generateExpress() {
|
|
186
|
+
return `---
|
|
187
|
+
description: Express/Node.js best practices
|
|
188
|
+
alwaysApply: true
|
|
189
|
+
globs: ["*.js", "*.ts"]
|
|
190
|
+
verify:
|
|
191
|
+
- antipattern: "app\\\\.use\\\\(express\\\\.json\\\\(\\\\)\\\\)"
|
|
192
|
+
message: "Consider adding body size limits to express.json()"
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
# Express Rules
|
|
196
|
+
|
|
197
|
+
- Use async/await with proper error handling
|
|
198
|
+
- Always validate and sanitize user input
|
|
199
|
+
- Use middleware for cross-cutting concerns
|
|
200
|
+
- Add rate limiting for public endpoints
|
|
201
|
+
`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function generatePython() {
|
|
205
|
+
return `---
|
|
206
|
+
description: Python best practices
|
|
207
|
+
alwaysApply: true
|
|
208
|
+
globs: ["*.py"]
|
|
209
|
+
verify:
|
|
210
|
+
- antipattern: "print\\\\("
|
|
211
|
+
message: "Use logging instead of print statements"
|
|
212
|
+
- antipattern: "import \\\\*"
|
|
213
|
+
message: "Avoid wildcard imports - import specific names"
|
|
214
|
+
- antipattern: "except:"
|
|
215
|
+
message: "Avoid bare except - catch specific exceptions"
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
# Python Rules
|
|
219
|
+
|
|
220
|
+
- Follow PEP 8 style guidelines
|
|
221
|
+
- Use type hints for function signatures
|
|
222
|
+
- Use logging instead of print statements
|
|
223
|
+
- Handle exceptions specifically, never use bare except
|
|
224
|
+
`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = { initProject, detectStack };
|
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 };
|