cursor-doctor 1.0.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/LICENSE +21 -0
- package/README.md +99 -0
- package/package.json +38 -0
- package/src/audit.js +342 -0
- package/src/autofix.js +171 -0
- package/src/cli.js +322 -0
- package/src/diff.js +93 -0
- package/src/doctor.js +218 -0
- package/src/fix.js +89 -0
- package/src/generate.js +698 -0
- package/src/index.js +570 -0
- package/src/init.js +227 -0
- package/src/license.js +88 -0
- package/src/migrate.js +118 -0
- package/src/order.js +131 -0
- package/src/plugin.js +653 -0
- package/src/stats.js +183 -0
- package/src/verify.js +304 -0
- package/src/versions.js +225 -0
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/license.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const https = require('https');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
|
|
6
|
+
const PRODUCT_PERMALINK = 'cursor-doctor-pro';
|
|
7
|
+
const LICENSE_FILE = '.cursor-doctor-license';
|
|
8
|
+
const SALT = 'cursor-doctor-v1';
|
|
9
|
+
|
|
10
|
+
function getLicensePath() {
|
|
11
|
+
const home = process.env.HOME || process.env.USERPROFILE || '.';
|
|
12
|
+
return path.join(home, LICENSE_FILE);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isLicensed() {
|
|
16
|
+
var p = getLicensePath();
|
|
17
|
+
if (!fs.existsSync(p)) return false;
|
|
18
|
+
try {
|
|
19
|
+
var stored = fs.readFileSync(p, 'utf-8').trim();
|
|
20
|
+
return stored.length === 64 && /^[a-f0-9]+$/.test(stored);
|
|
21
|
+
} catch (e) { return false; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function verifyWithGumroad(key) {
|
|
25
|
+
return new Promise(function(resolve) {
|
|
26
|
+
var postData = 'product_permalink=' + encodeURIComponent(PRODUCT_PERMALINK) + '&license_key=' + encodeURIComponent(key.trim());
|
|
27
|
+
|
|
28
|
+
var options = {
|
|
29
|
+
hostname: 'api.gumroad.com',
|
|
30
|
+
path: '/v2/licenses/verify',
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: {
|
|
33
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
34
|
+
'Content-Length': Buffer.byteLength(postData)
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
var req = https.request(options, function(res) {
|
|
39
|
+
var body = '';
|
|
40
|
+
res.on('data', function(chunk) { body += chunk; });
|
|
41
|
+
res.on('end', function() {
|
|
42
|
+
try {
|
|
43
|
+
var json = JSON.parse(body);
|
|
44
|
+
if (json.success === true && json.purchase && !json.purchase.refunded && !json.purchase.chargebacked) {
|
|
45
|
+
resolve({ valid: true });
|
|
46
|
+
} else if (json.success === false) {
|
|
47
|
+
resolve({ valid: false, error: 'Invalid license key' });
|
|
48
|
+
} else {
|
|
49
|
+
resolve({ valid: false, error: 'Key is refunded or chargebacked' });
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {
|
|
52
|
+
resolve({ valid: false, error: 'Could not parse Gumroad response' });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
req.on('error', function(e) {
|
|
58
|
+
resolve({ valid: false, error: 'Could not reach Gumroad: ' + e.message });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
req.write(postData);
|
|
62
|
+
req.end();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function activateLicense(dir, key) {
|
|
67
|
+
if (!key || key.trim().length < 8) {
|
|
68
|
+
return { ok: false, error: 'Key too short' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
var result = await verifyWithGumroad(key);
|
|
72
|
+
|
|
73
|
+
if (!result.valid) {
|
|
74
|
+
return { ok: false, error: result.error };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
var hash = crypto.createHash('sha256').update(SALT + ':' + key.trim()).digest('hex');
|
|
78
|
+
var homePath = getLicensePath();
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
fs.writeFileSync(homePath, hash + '\n', 'utf-8');
|
|
82
|
+
return { ok: true, path: homePath };
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return { ok: false, error: 'Failed to save license: ' + e.message };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { isLicensed, activateLicense };
|
package/src/migrate.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function migrate(dir) {
|
|
5
|
+
const cursorrules = path.join(dir, '.cursorrules');
|
|
6
|
+
const result = { created: [], skipped: [], source: null, error: null };
|
|
7
|
+
|
|
8
|
+
if (!fs.existsSync(cursorrules)) {
|
|
9
|
+
result.error = 'No .cursorrules file found in this directory';
|
|
10
|
+
return result;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const content = fs.readFileSync(cursorrules, 'utf-8').trim();
|
|
14
|
+
result.source = { file: '.cursorrules', chars: content.length, lines: content.split('\n').length };
|
|
15
|
+
|
|
16
|
+
if (content.length === 0) {
|
|
17
|
+
result.error = '.cursorrules file is empty';
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const rulesDir = path.join(dir, '.cursor', 'rules');
|
|
22
|
+
fs.mkdirSync(rulesDir, { recursive: true });
|
|
23
|
+
|
|
24
|
+
// Try to split by markdown headings (## or #)
|
|
25
|
+
const sections = splitBySections(content);
|
|
26
|
+
|
|
27
|
+
if (sections.length <= 1) {
|
|
28
|
+
// Single file migration
|
|
29
|
+
const filename = 'project-rules.mdc';
|
|
30
|
+
const destPath = path.join(rulesDir, filename);
|
|
31
|
+
if (fs.existsSync(destPath)) {
|
|
32
|
+
result.skipped.push(filename);
|
|
33
|
+
} else {
|
|
34
|
+
const mdc = wrapInMdc(content, 'Project rules migrated from .cursorrules');
|
|
35
|
+
fs.writeFileSync(destPath, mdc, 'utf-8');
|
|
36
|
+
result.created.push(filename);
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
// Multi-file migration
|
|
40
|
+
for (const section of sections) {
|
|
41
|
+
const filename = slugify(section.title) + '.mdc';
|
|
42
|
+
const destPath = path.join(rulesDir, filename);
|
|
43
|
+
if (fs.existsSync(destPath)) {
|
|
44
|
+
result.skipped.push(filename);
|
|
45
|
+
} else {
|
|
46
|
+
const mdc = wrapInMdc(section.body, section.title);
|
|
47
|
+
fs.writeFileSync(destPath, mdc, 'utf-8');
|
|
48
|
+
result.created.push(filename);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function splitBySections(content) {
|
|
57
|
+
const lines = content.split('\n');
|
|
58
|
+
const sections = [];
|
|
59
|
+
let currentTitle = null;
|
|
60
|
+
let currentBody = [];
|
|
61
|
+
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
const headingMatch = line.match(/^#{1,2}\s+(.+)/);
|
|
64
|
+
if (headingMatch) {
|
|
65
|
+
if (currentTitle !== null) {
|
|
66
|
+
sections.push({ title: currentTitle, body: currentBody.join('\n').trim() });
|
|
67
|
+
}
|
|
68
|
+
currentTitle = headingMatch[1].trim();
|
|
69
|
+
currentBody = [];
|
|
70
|
+
} else {
|
|
71
|
+
currentBody.push(line);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Push last section
|
|
76
|
+
if (currentTitle !== null) {
|
|
77
|
+
sections.push({ title: currentTitle, body: currentBody.join('\n').trim() });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// If no headings found, check for content before first heading
|
|
81
|
+
if (sections.length === 0) {
|
|
82
|
+
return [{ title: 'Project Rules', body: content }];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// If there's content before the first heading, include it
|
|
86
|
+
const firstHeadingIdx = content.search(/^#{1,2}\s+/m);
|
|
87
|
+
if (firstHeadingIdx > 0) {
|
|
88
|
+
const preamble = content.slice(0, firstHeadingIdx).trim();
|
|
89
|
+
if (preamble.length > 20) {
|
|
90
|
+
sections.unshift({ title: 'General', body: preamble });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Filter out empty sections
|
|
95
|
+
return sections.filter(s => s.body.length > 10);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function slugify(title) {
|
|
99
|
+
return title
|
|
100
|
+
.toLowerCase()
|
|
101
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
102
|
+
.replace(/\s+/g, '-')
|
|
103
|
+
.replace(/-+/g, '-')
|
|
104
|
+
.replace(/^-|-$/g, '')
|
|
105
|
+
.slice(0, 40) || 'rule';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function wrapInMdc(body, description) {
|
|
109
|
+
return `---
|
|
110
|
+
description: "${description.replace(/"/g, '\\"')}"
|
|
111
|
+
alwaysApply: true
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
${body}
|
|
115
|
+
`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = { migrate };
|
package/src/order.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function parseFrontmatter(content) {
|
|
5
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
6
|
+
if (!match) return { found: false, data: null };
|
|
7
|
+
|
|
8
|
+
const data = {};
|
|
9
|
+
const lines = match[1].split('\n');
|
|
10
|
+
for (const line of lines) {
|
|
11
|
+
const colonIdx = line.indexOf(':');
|
|
12
|
+
if (colonIdx === -1) continue;
|
|
13
|
+
const key = line.slice(0, colonIdx).trim();
|
|
14
|
+
const rawVal = line.slice(colonIdx + 1).trim();
|
|
15
|
+
if (rawVal === 'true') data[key] = true;
|
|
16
|
+
else if (rawVal === 'false') data[key] = false;
|
|
17
|
+
else if (rawVal.startsWith('"') && rawVal.endsWith('"')) data[key] = rawVal.slice(1, -1);
|
|
18
|
+
else data[key] = rawVal;
|
|
19
|
+
}
|
|
20
|
+
return { found: true, data };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseGlobs(globVal) {
|
|
24
|
+
if (!globVal) return [];
|
|
25
|
+
if (typeof globVal === 'string') {
|
|
26
|
+
const trimmed = globVal.trim();
|
|
27
|
+
if (trimmed.startsWith('[')) {
|
|
28
|
+
return trimmed.slice(1, -1).split(',').map(g => g.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
|
|
29
|
+
}
|
|
30
|
+
return trimmed.split(',').map(g => g.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
|
|
31
|
+
}
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function estimateTokens(text) {
|
|
36
|
+
// Rough estimate: ~4 chars per token for English text
|
|
37
|
+
return Math.ceil(text.length / 4);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function showLoadOrder(dir) {
|
|
41
|
+
const results = {
|
|
42
|
+
hasCursorrules: false,
|
|
43
|
+
rules: [],
|
|
44
|
+
warnings: [],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Check for .cursorrules
|
|
48
|
+
const cursorrules = path.join(dir, '.cursorrules');
|
|
49
|
+
if (fs.existsSync(cursorrules)) {
|
|
50
|
+
results.hasCursorrules = true;
|
|
51
|
+
const content = fs.readFileSync(cursorrules, 'utf-8');
|
|
52
|
+
const lines = content.split('\n').length;
|
|
53
|
+
results.rules.push({
|
|
54
|
+
file: '.cursorrules',
|
|
55
|
+
tier: 'always',
|
|
56
|
+
globs: [],
|
|
57
|
+
description: '(legacy format)',
|
|
58
|
+
alwaysApply: true,
|
|
59
|
+
lines,
|
|
60
|
+
tokens: estimateTokens(content),
|
|
61
|
+
priority: 0, // lowest priority — overridden by .mdc
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check .cursor/rules/*.mdc
|
|
66
|
+
const rulesDir = path.join(dir, '.cursor', 'rules');
|
|
67
|
+
if (fs.existsSync(rulesDir) && fs.statSync(rulesDir).isDirectory()) {
|
|
68
|
+
const files = fs.readdirSync(rulesDir).filter(f => f.endsWith('.mdc')).sort();
|
|
69
|
+
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
const filePath = path.join(rulesDir, file);
|
|
72
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
73
|
+
const fm = parseFrontmatter(content);
|
|
74
|
+
const lines = content.split('\n').length;
|
|
75
|
+
const tokens = estimateTokens(content);
|
|
76
|
+
|
|
77
|
+
if (!fm.found || !fm.data) {
|
|
78
|
+
results.rules.push({
|
|
79
|
+
file,
|
|
80
|
+
tier: 'manual',
|
|
81
|
+
globs: [],
|
|
82
|
+
description: '(no frontmatter)',
|
|
83
|
+
alwaysApply: false,
|
|
84
|
+
lines,
|
|
85
|
+
tokens,
|
|
86
|
+
});
|
|
87
|
+
results.warnings.push(`${file}: Missing frontmatter — rule may not load at all`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const globs = parseGlobs(fm.data.globs);
|
|
92
|
+
const alwaysApply = fm.data.alwaysApply === true;
|
|
93
|
+
const description = fm.data.description || '';
|
|
94
|
+
|
|
95
|
+
let tier;
|
|
96
|
+
if (alwaysApply) {
|
|
97
|
+
tier = 'always';
|
|
98
|
+
} else if (globs.length > 0) {
|
|
99
|
+
tier = 'glob';
|
|
100
|
+
} else {
|
|
101
|
+
tier = 'manual';
|
|
102
|
+
results.warnings.push(`${file}: No alwaysApply and no globs — this rule may never activate in agent mode`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
results.rules.push({
|
|
106
|
+
file,
|
|
107
|
+
tier,
|
|
108
|
+
globs,
|
|
109
|
+
description,
|
|
110
|
+
alwaysApply,
|
|
111
|
+
lines,
|
|
112
|
+
tokens,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Sort within tiers: always first, then glob, then manual
|
|
118
|
+
// Within each tier, sort by filename (alphabetical = filesystem order)
|
|
119
|
+
const tierOrder = { always: 0, glob: 1, manual: 2 };
|
|
120
|
+
results.rules.sort((a, b) => {
|
|
121
|
+
if (tierOrder[a.tier] !== tierOrder[b.tier]) return tierOrder[a.tier] - tierOrder[b.tier];
|
|
122
|
+
// .cursorrules always last within 'always' tier (lowest priority)
|
|
123
|
+
if (a.file === '.cursorrules') return -1;
|
|
124
|
+
if (b.file === '.cursorrules') return 1;
|
|
125
|
+
return a.file.localeCompare(b.file);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return results;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = { showLoadOrder };
|