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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-lint",
3
- "version": "0.1.1",
3
+ "version": "0.3.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
@@ -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 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();
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
- 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}`);
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
- 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++;
157
+ console.log();
71
158
  }
72
- console.log();
73
- }
74
159
 
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');
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
- process.exit(totalErrors > 0 ? 1 : 0);
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 };