cursor-lint 0.6.0 ā 0.7.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 +71 -2
- package/src/order.js +131 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ const { initProject } = require('./init');
|
|
|
7
7
|
const { fixProject } = require('./fix');
|
|
8
8
|
const { generateRules } = require('./generate');
|
|
9
9
|
|
|
10
|
-
const VERSION = '0.
|
|
10
|
+
const VERSION = '0.7.0';
|
|
11
11
|
|
|
12
12
|
const RED = '\x1b[31m';
|
|
13
13
|
const YELLOW = '\x1b[33m';
|
|
@@ -32,6 +32,7 @@ ${YELLOW}Options:${RESET}
|
|
|
32
32
|
--init Generate starter .mdc rules (auto-detects your stack)
|
|
33
33
|
--fix Auto-fix common issues (missing frontmatter, alwaysApply)
|
|
34
34
|
--generate Auto-detect stack & download matching .mdc rules from GitHub
|
|
35
|
+
--order Show rule load order, priority tiers, and token estimates
|
|
35
36
|
|
|
36
37
|
${YELLOW}What it checks (default):${RESET}
|
|
37
38
|
⢠.cursorrules files (warns about agent mode compatibility)
|
|
@@ -88,8 +89,76 @@ async function main() {
|
|
|
88
89
|
const isInit = args.includes('--init');
|
|
89
90
|
const isFix = args.includes('--fix');
|
|
90
91
|
const isGenerate = args.includes('--generate');
|
|
92
|
+
const isOrder = args.includes('--order');
|
|
91
93
|
|
|
92
|
-
if (
|
|
94
|
+
if (isOrder) {
|
|
95
|
+
const { showLoadOrder } = require('./order');
|
|
96
|
+
console.log(`\nš cursor-lint v${VERSION} --order\n`);
|
|
97
|
+
const dir = args.find(a => !a.startsWith('-')) ? path.resolve(args.find(a => !a.startsWith('-'))) : cwd;
|
|
98
|
+
console.log(`Analyzing rule load order in ${dir}...\n`);
|
|
99
|
+
|
|
100
|
+
const results = showLoadOrder(dir);
|
|
101
|
+
|
|
102
|
+
if (results.rules.length === 0) {
|
|
103
|
+
console.log(`${YELLOW}No rules found.${RESET}\n`);
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Show .cursorrules warning if present
|
|
108
|
+
if (results.hasCursorrules) {
|
|
109
|
+
console.log(`${YELLOW}ā .cursorrules found${RESET} ā overridden by any .mdc rule covering the same topic`);
|
|
110
|
+
console.log(`${DIM} .mdc files always take precedence when both exist${RESET}\n`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Group by priority tier
|
|
114
|
+
const tiers = {
|
|
115
|
+
'always': { label: 'Always Active', color: GREEN, rules: [] },
|
|
116
|
+
'glob': { label: 'File-Scoped (glob match)', color: CYAN, rules: [] },
|
|
117
|
+
'manual': { label: 'Manual Only (no alwaysApply, no globs)', color: DIM, rules: [] },
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
for (const rule of results.rules) {
|
|
121
|
+
tiers[rule.tier].rules.push(rule);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let position = 1;
|
|
125
|
+
for (const [key, tier] of Object.entries(tiers)) {
|
|
126
|
+
if (tier.rules.length === 0) continue;
|
|
127
|
+
console.log(`${tier.color}āā ${tier.label} āā${RESET}`);
|
|
128
|
+
for (const rule of tier.rules) {
|
|
129
|
+
const globs = rule.globs.length > 0 ? ` ${DIM}[${rule.globs.join(', ')}]${RESET}` : '';
|
|
130
|
+
const desc = rule.description ? ` ${DIM}ā ${rule.description}${RESET}` : '';
|
|
131
|
+
const size = ` ${DIM}(${rule.lines} lines, ~${rule.tokens} tokens)${RESET}`;
|
|
132
|
+
console.log(` ${position}. ${rule.file}${globs}${desc}${size}`);
|
|
133
|
+
position++;
|
|
134
|
+
}
|
|
135
|
+
console.log();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Token budget warning
|
|
139
|
+
const totalTokens = results.rules.reduce((s, r) => s + r.tokens, 0);
|
|
140
|
+
const alwaysTokens = tiers.always.rules.reduce((s, r) => s + r.tokens, 0);
|
|
141
|
+
console.log('ā'.repeat(50));
|
|
142
|
+
console.log(`${CYAN}Total rules:${RESET} ${results.rules.length}`);
|
|
143
|
+
console.log(`${CYAN}Always-active token estimate:${RESET} ~${alwaysTokens} tokens`);
|
|
144
|
+
console.log(`${CYAN}All rules token estimate:${RESET} ~${totalTokens} tokens`);
|
|
145
|
+
|
|
146
|
+
if (alwaysTokens > 4000) {
|
|
147
|
+
console.log(`\n${YELLOW}ā Your always-active rules use ~${alwaysTokens} tokens.${RESET}`);
|
|
148
|
+
console.log(`${DIM} Large rule sets eat into your context window. Consider moving some to glob-scoped rules.${RESET}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (results.warnings.length > 0) {
|
|
152
|
+
console.log();
|
|
153
|
+
for (const w of results.warnings) {
|
|
154
|
+
console.log(`${YELLOW}ā ${w}${RESET}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log();
|
|
159
|
+
process.exit(0);
|
|
160
|
+
|
|
161
|
+
} else if (isGenerate) {
|
|
93
162
|
console.log(`\nš cursor-lint v${VERSION} --generate\n`);
|
|
94
163
|
console.log(`Detecting stack in ${cwd}...\n`);
|
|
95
164
|
|
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 };
|