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.
- package/package.json +2 -2
- package/src/cli.js +153 -64
- 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,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
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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 };
|