cursor-lint 0.1.0 → 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.
- package/package.json +2 -2
- package/src/cli.js +153 -65
- package/src/index.js +0 -18
- package/src/verify.js +304 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-lint",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Lint your Cursor rules
|
|
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,85 +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
|
|
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();
|
|
7
|
+
const VERSION = '0.2.0';
|
|
34
8
|
|
|
35
9
|
const RED = '\x1b[31m';
|
|
36
10
|
const YELLOW = '\x1b[33m';
|
|
37
11
|
const GREEN = '\x1b[32m';
|
|
12
|
+
const CYAN = '\x1b[36m';
|
|
38
13
|
const DIM = '\x1b[2m';
|
|
39
14
|
const RESET = '\x1b[0m';
|
|
40
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
|
+
|
|
41
65
|
async function main() {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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}`);
|
|
65
116
|
}
|
|
66
117
|
}
|
|
67
|
-
|
|
68
|
-
const warnings = result.issues.filter(i => i.severity === 'warning').length;
|
|
69
|
-
totalErrors += errors;
|
|
70
|
-
totalWarnings += warnings;
|
|
71
|
-
if (errors === 0 && warnings === 0) totalPassed++;
|
|
118
|
+
console.log();
|
|
72
119
|
}
|
|
73
|
-
console.log();
|
|
74
|
-
}
|
|
75
120
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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');
|
|
82
169
|
|
|
83
|
-
|
|
170
|
+
process.exit(totalErrors > 0 ? 1 : 0);
|
|
171
|
+
}
|
|
84
172
|
}
|
|
85
173
|
|
|
86
174
|
main().catch(err => {
|
package/src/index.js
CHANGED
|
@@ -28,9 +28,6 @@ const VAGUE_PATTERNS = [
|
|
|
28
28
|
'be concise',
|
|
29
29
|
];
|
|
30
30
|
|
|
31
|
-
const MAX_LINES_WARNING = 150;
|
|
32
|
-
const MAX_LINES_ERROR = 300;
|
|
33
|
-
|
|
34
31
|
function parseFrontmatter(content) {
|
|
35
32
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
36
33
|
if (!match) return { found: false, data: null, error: null };
|
|
@@ -65,7 +62,6 @@ function parseFrontmatter(content) {
|
|
|
65
62
|
async function lintMdcFile(filePath) {
|
|
66
63
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
67
64
|
const issues = [];
|
|
68
|
-
const lines = content.split('\n');
|
|
69
65
|
|
|
70
66
|
const fm = parseFrontmatter(content);
|
|
71
67
|
|
|
@@ -95,13 +91,6 @@ async function lintMdcFile(filePath) {
|
|
|
95
91
|
}
|
|
96
92
|
}
|
|
97
93
|
|
|
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
94
|
return { file: filePath, issues };
|
|
106
95
|
}
|
|
107
96
|
|
|
@@ -125,13 +114,6 @@ async function lintCursorrules(filePath) {
|
|
|
125
114
|
}
|
|
126
115
|
}
|
|
127
116
|
|
|
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
117
|
return { file: filePath, issues };
|
|
136
118
|
}
|
|
137
119
|
|
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 };
|