@yawlabs/ctxlint 0.2.2 → 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/README.md +54 -17
- package/dist/index.js +706 -70
- package/dist/mcp/server.js +566 -47
- package/package.json +25 -11
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
// src/index.ts
|
|
10
|
+
import * as fs6 from "fs";
|
|
10
11
|
import { Command } from "commander";
|
|
11
12
|
import ora from "ora";
|
|
12
13
|
|
|
@@ -93,43 +94,76 @@ function getAllProjectFiles(projectRoot) {
|
|
|
93
94
|
|
|
94
95
|
// src/core/scanner.ts
|
|
95
96
|
var CONTEXT_FILE_PATTERNS = [
|
|
97
|
+
// Claude Code
|
|
96
98
|
"CLAUDE.md",
|
|
97
99
|
"CLAUDE.local.md",
|
|
100
|
+
".claude/rules/*.md",
|
|
101
|
+
// AGENTS.md (AAIF / Linux Foundation standard)
|
|
98
102
|
"AGENTS.md",
|
|
103
|
+
"AGENT.md",
|
|
104
|
+
"AGENTS.override.md",
|
|
105
|
+
// Cursor
|
|
99
106
|
".cursorrules",
|
|
100
107
|
".cursor/rules/*.md",
|
|
101
108
|
".cursor/rules/*.mdc",
|
|
102
|
-
"
|
|
109
|
+
".cursor/rules/*/RULE.md",
|
|
110
|
+
// GitHub Copilot
|
|
103
111
|
".github/copilot-instructions.md",
|
|
104
112
|
".github/instructions/*.md",
|
|
113
|
+
".github/git-commit-instructions.md",
|
|
114
|
+
// Windsurf
|
|
105
115
|
".windsurfrules",
|
|
106
116
|
".windsurf/rules/*.md",
|
|
117
|
+
// Gemini CLI
|
|
107
118
|
"GEMINI.md",
|
|
108
|
-
|
|
119
|
+
// Cline
|
|
109
120
|
".clinerules",
|
|
110
|
-
|
|
111
|
-
"CODEX.md",
|
|
121
|
+
// Aider — note: .aiderules has no file extension; this is the intended format
|
|
112
122
|
".aiderules",
|
|
123
|
+
// Aide / Codestory
|
|
113
124
|
".aide/rules/*.md",
|
|
125
|
+
// Amazon Q Developer
|
|
114
126
|
".amazonq/rules/*.md",
|
|
115
|
-
|
|
127
|
+
// Goose (Block)
|
|
128
|
+
".goose/instructions.md",
|
|
129
|
+
".goosehints",
|
|
130
|
+
// JetBrains Junie
|
|
131
|
+
".junie/guidelines.md",
|
|
132
|
+
".junie/AGENTS.md",
|
|
133
|
+
// JetBrains AI Assistant
|
|
134
|
+
".aiassistant/rules/*.md",
|
|
135
|
+
// Continue
|
|
136
|
+
".continuerules",
|
|
137
|
+
".continue/rules/*.md",
|
|
138
|
+
// Zed
|
|
139
|
+
".rules",
|
|
140
|
+
// Replit
|
|
141
|
+
"replit.md"
|
|
116
142
|
];
|
|
117
143
|
var IGNORED_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "vendor"]);
|
|
118
|
-
async function scanForContextFiles(projectRoot) {
|
|
144
|
+
async function scanForContextFiles(projectRoot, options = {}) {
|
|
145
|
+
const maxDepth = options.depth ?? 2;
|
|
146
|
+
const patterns = [...CONTEXT_FILE_PATTERNS, ...options.extraPatterns || []];
|
|
119
147
|
const found = [];
|
|
120
148
|
const seen = /* @__PURE__ */ new Set();
|
|
121
149
|
const dirsToScan = [projectRoot];
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
150
|
+
function collectDirs(dir, currentDepth) {
|
|
151
|
+
if (currentDepth >= maxDepth) return;
|
|
152
|
+
try {
|
|
153
|
+
const entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
154
|
+
for (const entry of entries) {
|
|
155
|
+
if (entry.isDirectory() && !IGNORED_DIRS2.has(entry.name) && !entry.name.startsWith(".")) {
|
|
156
|
+
const fullPath = path2.join(dir, entry.name);
|
|
157
|
+
dirsToScan.push(fullPath);
|
|
158
|
+
collectDirs(fullPath, currentDepth + 1);
|
|
159
|
+
}
|
|
127
160
|
}
|
|
161
|
+
} catch {
|
|
128
162
|
}
|
|
129
|
-
} catch {
|
|
130
163
|
}
|
|
164
|
+
collectDirs(projectRoot, 0);
|
|
131
165
|
for (const dir of dirsToScan) {
|
|
132
|
-
for (const pattern of
|
|
166
|
+
for (const pattern of patterns) {
|
|
133
167
|
const matches = await glob(pattern, {
|
|
134
168
|
cwd: dir,
|
|
135
169
|
absolute: true,
|
|
@@ -570,6 +604,7 @@ import * as fs3 from "fs";
|
|
|
570
604
|
import * as path4 from "path";
|
|
571
605
|
var NPM_SCRIPT_PATTERN = /^(?:npm\s+run|pnpm(?:\s+run)?|yarn(?:\s+run)?|bun(?:\s+run)?)\s+(\S+)/;
|
|
572
606
|
var MAKE_PATTERN = /^make\s+(\S+)/;
|
|
607
|
+
var NPX_PATTERN = /^npx\s+(\S+)/;
|
|
573
608
|
async function checkCommands(file, projectRoot) {
|
|
574
609
|
const issues = [];
|
|
575
610
|
const pkgJson = loadPackageJson(projectRoot);
|
|
@@ -591,7 +626,9 @@ async function checkCommands(file, projectRoot) {
|
|
|
591
626
|
}
|
|
592
627
|
continue;
|
|
593
628
|
}
|
|
594
|
-
const shorthandMatch = cmd.match(
|
|
629
|
+
const shorthandMatch = cmd.match(
|
|
630
|
+
/^(npm|pnpm|yarn|bun)\s+(test|start|build|dev|lint|format|check|typecheck|clean|serve|preview|e2e)\b/
|
|
631
|
+
);
|
|
595
632
|
if (shorthandMatch && pkgJson) {
|
|
596
633
|
const scriptName = shorthandMatch[2];
|
|
597
634
|
if (pkgJson.scripts && !(scriptName in pkgJson.scripts)) {
|
|
@@ -604,6 +641,30 @@ async function checkCommands(file, projectRoot) {
|
|
|
604
641
|
}
|
|
605
642
|
continue;
|
|
606
643
|
}
|
|
644
|
+
const npxMatch = cmd.match(NPX_PATTERN);
|
|
645
|
+
if (npxMatch && pkgJson) {
|
|
646
|
+
const pkgName = npxMatch[1];
|
|
647
|
+
if (pkgName.startsWith("-")) continue;
|
|
648
|
+
const allDeps = {
|
|
649
|
+
...pkgJson.dependencies,
|
|
650
|
+
...pkgJson.devDependencies
|
|
651
|
+
};
|
|
652
|
+
if (!(pkgName in allDeps)) {
|
|
653
|
+
const binPath = path4.join(projectRoot, "node_modules", ".bin", pkgName);
|
|
654
|
+
try {
|
|
655
|
+
fs3.accessSync(binPath);
|
|
656
|
+
} catch {
|
|
657
|
+
issues.push({
|
|
658
|
+
severity: "warning",
|
|
659
|
+
check: "commands",
|
|
660
|
+
line: ref.line,
|
|
661
|
+
message: `"${cmd}" \u2014 "${pkgName}" not found in dependencies`,
|
|
662
|
+
suggestion: "If this is a global tool, consider adding it to devDependencies for reproducibility"
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
607
668
|
const makeMatch = cmd.match(MAKE_PATTERN);
|
|
608
669
|
if (makeMatch) {
|
|
609
670
|
const target = makeMatch[1];
|
|
@@ -822,6 +883,26 @@ var PACKAGE_TECH_MAP = {
|
|
|
822
883
|
cypress: ["Cypress"],
|
|
823
884
|
puppeteer: ["Puppeteer"]
|
|
824
885
|
};
|
|
886
|
+
function compilePatterns(allDeps) {
|
|
887
|
+
const compiled = [];
|
|
888
|
+
for (const [pkg, mentions] of Object.entries(PACKAGE_TECH_MAP)) {
|
|
889
|
+
if (!allDeps.has(pkg)) continue;
|
|
890
|
+
for (const mention of mentions) {
|
|
891
|
+
const escaped = escapeRegex(mention);
|
|
892
|
+
compiled.push({
|
|
893
|
+
pkg,
|
|
894
|
+
mention,
|
|
895
|
+
patterns: [
|
|
896
|
+
new RegExp(`\\b(?:use|using|built with|powered by|written in)\\s+${escaped}\\b`, "i"),
|
|
897
|
+
new RegExp(`\\bwe\\s+use\\s+${escaped}\\b`, "i"),
|
|
898
|
+
new RegExp(`\\b${escaped}\\s+(?:project|app|application|codebase)\\b`, "i"),
|
|
899
|
+
new RegExp(`\\bThis is a\\s+${escaped}\\b`, "i")
|
|
900
|
+
]
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return compiled;
|
|
905
|
+
}
|
|
825
906
|
async function checkRedundancy(file, projectRoot) {
|
|
826
907
|
const issues = [];
|
|
827
908
|
const pkgJson = loadPackageJson(projectRoot);
|
|
@@ -830,38 +911,28 @@ async function checkRedundancy(file, projectRoot) {
|
|
|
830
911
|
...Object.keys(pkgJson.dependencies || {}),
|
|
831
912
|
...Object.keys(pkgJson.devDependencies || {})
|
|
832
913
|
]);
|
|
914
|
+
const compiledPatterns = compilePatterns(allDeps);
|
|
833
915
|
const lines2 = file.content.split("\n");
|
|
834
916
|
for (let i = 0; i < lines2.length; i++) {
|
|
835
917
|
const line = lines2[i];
|
|
836
|
-
for (const
|
|
837
|
-
|
|
838
|
-
for (const
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
"i"
|
|
843
|
-
),
|
|
844
|
-
new RegExp(`\\bwe\\s+use\\s+${escapeRegex(mention)}\\b`, "i"),
|
|
845
|
-
new RegExp(
|
|
846
|
-
`\\b${escapeRegex(mention)}\\s+(?:project|app|application|codebase)\\b`,
|
|
847
|
-
"i"
|
|
848
|
-
),
|
|
849
|
-
new RegExp(`\\bThis is a\\s+${escapeRegex(mention)}\\b`, "i")
|
|
850
|
-
];
|
|
851
|
-
for (const pattern of patterns) {
|
|
852
|
-
if (pattern.test(line)) {
|
|
853
|
-
const wastedTokens = countTokens(line.trim());
|
|
854
|
-
issues.push({
|
|
855
|
-
severity: "info",
|
|
856
|
-
check: "redundancy",
|
|
857
|
-
line: i + 1,
|
|
858
|
-
message: `"${mention}" is in package.json ${pkgJson.dependencies?.[pkg] ? "dependencies" : "devDependencies"} \u2014 agent can infer this`,
|
|
859
|
-
suggestion: `~${wastedTokens} tokens could be saved`
|
|
860
|
-
});
|
|
861
|
-
break;
|
|
862
|
-
}
|
|
918
|
+
for (const { pkg, mention, patterns } of compiledPatterns) {
|
|
919
|
+
let matched = false;
|
|
920
|
+
for (const pattern of patterns) {
|
|
921
|
+
if (pattern.test(line)) {
|
|
922
|
+
matched = true;
|
|
923
|
+
break;
|
|
863
924
|
}
|
|
864
925
|
}
|
|
926
|
+
if (matched) {
|
|
927
|
+
const wastedTokens = countTokens(line.trim());
|
|
928
|
+
issues.push({
|
|
929
|
+
severity: "info",
|
|
930
|
+
check: "redundancy",
|
|
931
|
+
line: i + 1,
|
|
932
|
+
message: `"${mention}" is in package.json ${pkgJson.dependencies?.[pkg] ? "dependencies" : "devDependencies"} \u2014 agent can infer this`,
|
|
933
|
+
suggestion: `~${wastedTokens} tokens could be saved`
|
|
934
|
+
});
|
|
935
|
+
}
|
|
865
936
|
}
|
|
866
937
|
}
|
|
867
938
|
}
|
|
@@ -923,6 +994,427 @@ function escapeRegex(str) {
|
|
|
923
994
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
924
995
|
}
|
|
925
996
|
|
|
997
|
+
// src/core/checks/contradictions.ts
|
|
998
|
+
var DIRECTIVE_CATEGORIES = [
|
|
999
|
+
{
|
|
1000
|
+
name: "testing framework",
|
|
1001
|
+
options: [
|
|
1002
|
+
{
|
|
1003
|
+
label: "Jest",
|
|
1004
|
+
patterns: [/\buse\s+jest\b/i, /\bjest\s+for\s+test/i, /\btest.*with\s+jest\b/i]
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
label: "Vitest",
|
|
1008
|
+
patterns: [/\buse\s+vitest\b/i, /\bvitest\s+for\s+test/i, /\btest.*with\s+vitest\b/i]
|
|
1009
|
+
},
|
|
1010
|
+
{
|
|
1011
|
+
label: "Mocha",
|
|
1012
|
+
patterns: [/\buse\s+mocha\b/i, /\bmocha\s+for\s+test/i, /\btest.*with\s+mocha\b/i]
|
|
1013
|
+
},
|
|
1014
|
+
{
|
|
1015
|
+
label: "pytest",
|
|
1016
|
+
patterns: [/\buse\s+pytest\b/i, /\bpytest\s+for\s+test/i, /\btest.*with\s+pytest\b/i]
|
|
1017
|
+
},
|
|
1018
|
+
{
|
|
1019
|
+
label: "Playwright",
|
|
1020
|
+
patterns: [/\buse\s+playwright\b/i, /\bplaywright\s+for\s+(?:e2e|test)/i]
|
|
1021
|
+
},
|
|
1022
|
+
{ label: "Cypress", patterns: [/\buse\s+cypress\b/i, /\bcypress\s+for\s+(?:e2e|test)/i] }
|
|
1023
|
+
]
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
name: "package manager",
|
|
1027
|
+
options: [
|
|
1028
|
+
{
|
|
1029
|
+
label: "npm",
|
|
1030
|
+
patterns: [
|
|
1031
|
+
/\buse\s+npm\b/i,
|
|
1032
|
+
/\bnpm\s+as\s+(?:the\s+)?package\s+manager/i,
|
|
1033
|
+
/\balways\s+use\s+npm\b/i
|
|
1034
|
+
]
|
|
1035
|
+
},
|
|
1036
|
+
{
|
|
1037
|
+
label: "pnpm",
|
|
1038
|
+
patterns: [
|
|
1039
|
+
/\buse\s+pnpm\b/i,
|
|
1040
|
+
/\bpnpm\s+as\s+(?:the\s+)?package\s+manager/i,
|
|
1041
|
+
/\balways\s+use\s+pnpm\b/i
|
|
1042
|
+
]
|
|
1043
|
+
},
|
|
1044
|
+
{
|
|
1045
|
+
label: "yarn",
|
|
1046
|
+
patterns: [
|
|
1047
|
+
/\buse\s+yarn\b/i,
|
|
1048
|
+
/\byarn\s+as\s+(?:the\s+)?package\s+manager/i,
|
|
1049
|
+
/\balways\s+use\s+yarn\b/i
|
|
1050
|
+
]
|
|
1051
|
+
},
|
|
1052
|
+
{
|
|
1053
|
+
label: "bun",
|
|
1054
|
+
patterns: [
|
|
1055
|
+
/\buse\s+bun\b/i,
|
|
1056
|
+
/\bbun\s+as\s+(?:the\s+)?package\s+manager/i,
|
|
1057
|
+
/\balways\s+use\s+bun\b/i
|
|
1058
|
+
]
|
|
1059
|
+
}
|
|
1060
|
+
]
|
|
1061
|
+
},
|
|
1062
|
+
{
|
|
1063
|
+
name: "indentation style",
|
|
1064
|
+
options: [
|
|
1065
|
+
{
|
|
1066
|
+
label: "tabs",
|
|
1067
|
+
patterns: [/\buse\s+tabs\b/i, /\btab\s+indentation\b/i, /\bindent\s+with\s+tabs\b/i]
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
label: "2 spaces",
|
|
1071
|
+
patterns: [
|
|
1072
|
+
/\b2[\s-]?space\s+indent/i,
|
|
1073
|
+
/\bindent\s+with\s+2\s+spaces/i,
|
|
1074
|
+
/\b2[\s-]?space\s+tabs?\b/i
|
|
1075
|
+
]
|
|
1076
|
+
},
|
|
1077
|
+
{
|
|
1078
|
+
label: "4 spaces",
|
|
1079
|
+
patterns: [
|
|
1080
|
+
/\b4[\s-]?space\s+indent/i,
|
|
1081
|
+
/\bindent\s+with\s+4\s+spaces/i,
|
|
1082
|
+
/\b4[\s-]?space\s+tabs?\b/i
|
|
1083
|
+
]
|
|
1084
|
+
}
|
|
1085
|
+
]
|
|
1086
|
+
},
|
|
1087
|
+
{
|
|
1088
|
+
name: "semicolons",
|
|
1089
|
+
options: [
|
|
1090
|
+
{
|
|
1091
|
+
label: "semicolons",
|
|
1092
|
+
patterns: [
|
|
1093
|
+
/\buse\s+semicolons\b/i,
|
|
1094
|
+
/\balways\s+(?:use\s+)?semicolons\b/i,
|
|
1095
|
+
/\bsemicolons:\s*(?:true|yes)\b/i
|
|
1096
|
+
]
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
label: "no semicolons",
|
|
1100
|
+
patterns: [
|
|
1101
|
+
/\bno\s+semicolons\b/i,
|
|
1102
|
+
/\bavoid\s+semicolons\b/i,
|
|
1103
|
+
/\bomit\s+semicolons\b/i,
|
|
1104
|
+
/\bsemicolons:\s*(?:false|no)\b/i
|
|
1105
|
+
]
|
|
1106
|
+
}
|
|
1107
|
+
]
|
|
1108
|
+
},
|
|
1109
|
+
{
|
|
1110
|
+
name: "quote style",
|
|
1111
|
+
options: [
|
|
1112
|
+
{
|
|
1113
|
+
label: "single quotes",
|
|
1114
|
+
patterns: [
|
|
1115
|
+
/\bsingle\s+quotes?\b/i,
|
|
1116
|
+
/\buse\s+(?:single\s+)?['']single['']?\s+quotes?\b/i,
|
|
1117
|
+
/\bprefer\s+single\s+quotes?\b/i
|
|
1118
|
+
]
|
|
1119
|
+
},
|
|
1120
|
+
{
|
|
1121
|
+
label: "double quotes",
|
|
1122
|
+
patterns: [
|
|
1123
|
+
/\bdouble\s+quotes?\b/i,
|
|
1124
|
+
/\buse\s+(?:double\s+)?[""]double[""]?\s+quotes?\b/i,
|
|
1125
|
+
/\bprefer\s+double\s+quotes?\b/i
|
|
1126
|
+
]
|
|
1127
|
+
}
|
|
1128
|
+
]
|
|
1129
|
+
},
|
|
1130
|
+
{
|
|
1131
|
+
name: "naming convention",
|
|
1132
|
+
options: [
|
|
1133
|
+
{
|
|
1134
|
+
label: "camelCase",
|
|
1135
|
+
patterns: [/\bcamelCase\b/, /\bcamel[\s-]?case\s+(?:for|naming|convention)/i]
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
label: "snake_case",
|
|
1139
|
+
patterns: [/\bsnake_case\b/, /\bsnake[\s-]?case\s+(?:for|naming|convention)/i]
|
|
1140
|
+
},
|
|
1141
|
+
{
|
|
1142
|
+
label: "PascalCase",
|
|
1143
|
+
patterns: [/\bPascalCase\b/, /\bpascal[\s-]?case\s+(?:for|naming|convention)/i]
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
label: "kebab-case",
|
|
1147
|
+
patterns: [/\bkebab-case\b/, /\bkebab[\s-]?case\s+(?:for|naming|convention)/i]
|
|
1148
|
+
}
|
|
1149
|
+
]
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
name: "CSS approach",
|
|
1153
|
+
options: [
|
|
1154
|
+
{ label: "Tailwind", patterns: [/\buse\s+tailwind/i, /\btailwind\s+for\s+styl/i] },
|
|
1155
|
+
{
|
|
1156
|
+
label: "CSS Modules",
|
|
1157
|
+
patterns: [/\buse\s+css\s+modules\b/i, /\bcss\s+modules\s+for\s+styl/i]
|
|
1158
|
+
},
|
|
1159
|
+
{
|
|
1160
|
+
label: "styled-components",
|
|
1161
|
+
patterns: [/\buse\s+styled[\s-]?components\b/i, /\bstyled[\s-]?components\s+for\s+styl/i]
|
|
1162
|
+
},
|
|
1163
|
+
{ label: "CSS-in-JS", patterns: [/\buse\s+css[\s-]?in[\s-]?js\b/i] }
|
|
1164
|
+
]
|
|
1165
|
+
},
|
|
1166
|
+
{
|
|
1167
|
+
name: "state management",
|
|
1168
|
+
options: [
|
|
1169
|
+
{ label: "Redux", patterns: [/\buse\s+redux\b/i, /\bredux\s+for\s+state/i] },
|
|
1170
|
+
{ label: "Zustand", patterns: [/\buse\s+zustand\b/i, /\bzustand\s+for\s+state/i] },
|
|
1171
|
+
{ label: "MobX", patterns: [/\buse\s+mobx\b/i, /\bmobx\s+for\s+state/i] },
|
|
1172
|
+
{ label: "Jotai", patterns: [/\buse\s+jotai\b/i, /\bjotai\s+for\s+state/i] },
|
|
1173
|
+
{ label: "Recoil", patterns: [/\buse\s+recoil\b/i, /\brecoil\s+for\s+state/i] }
|
|
1174
|
+
]
|
|
1175
|
+
}
|
|
1176
|
+
];
|
|
1177
|
+
function detectDirectives(file) {
|
|
1178
|
+
const directives = [];
|
|
1179
|
+
const lines = file.content.split("\n");
|
|
1180
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1181
|
+
const line = lines[i];
|
|
1182
|
+
for (const category of DIRECTIVE_CATEGORIES) {
|
|
1183
|
+
for (const option of category.options) {
|
|
1184
|
+
for (const pattern of option.patterns) {
|
|
1185
|
+
if (pattern.test(line)) {
|
|
1186
|
+
directives.push({
|
|
1187
|
+
file: file.relativePath,
|
|
1188
|
+
category: category.name,
|
|
1189
|
+
label: option.label,
|
|
1190
|
+
line: i + 1,
|
|
1191
|
+
text: line.trim()
|
|
1192
|
+
});
|
|
1193
|
+
break;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return directives;
|
|
1200
|
+
}
|
|
1201
|
+
function checkContradictions(files) {
|
|
1202
|
+
if (files.length < 2) return [];
|
|
1203
|
+
const issues = [];
|
|
1204
|
+
const allDirectives = [];
|
|
1205
|
+
for (const file of files) {
|
|
1206
|
+
allDirectives.push(...detectDirectives(file));
|
|
1207
|
+
}
|
|
1208
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
1209
|
+
for (const d of allDirectives) {
|
|
1210
|
+
const existing = byCategory.get(d.category) || [];
|
|
1211
|
+
existing.push(d);
|
|
1212
|
+
byCategory.set(d.category, existing);
|
|
1213
|
+
}
|
|
1214
|
+
for (const [category, directives] of byCategory) {
|
|
1215
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1216
|
+
for (const d of directives) {
|
|
1217
|
+
const existing = byFile.get(d.file) || [];
|
|
1218
|
+
existing.push(d);
|
|
1219
|
+
byFile.set(d.file, existing);
|
|
1220
|
+
}
|
|
1221
|
+
const labels = new Set(directives.map((d) => d.label));
|
|
1222
|
+
if (labels.size <= 1) continue;
|
|
1223
|
+
const fileLabels = /* @__PURE__ */ new Map();
|
|
1224
|
+
for (const d of directives) {
|
|
1225
|
+
const existing = fileLabels.get(d.file) || /* @__PURE__ */ new Set();
|
|
1226
|
+
existing.add(d.label);
|
|
1227
|
+
fileLabels.set(d.file, existing);
|
|
1228
|
+
}
|
|
1229
|
+
const fileEntries = [...fileLabels.entries()];
|
|
1230
|
+
for (let i = 0; i < fileEntries.length; i++) {
|
|
1231
|
+
for (let j = i + 1; j < fileEntries.length; j++) {
|
|
1232
|
+
const [fileA, labelsA] = fileEntries[i];
|
|
1233
|
+
const [fileB, labelsB] = fileEntries[j];
|
|
1234
|
+
for (const labelA of labelsA) {
|
|
1235
|
+
for (const labelB of labelsB) {
|
|
1236
|
+
if (labelA !== labelB) {
|
|
1237
|
+
const directiveA = directives.find((d) => d.file === fileA && d.label === labelA);
|
|
1238
|
+
const directiveB = directives.find((d) => d.file === fileB && d.label === labelB);
|
|
1239
|
+
issues.push({
|
|
1240
|
+
severity: "warning",
|
|
1241
|
+
check: "contradictions",
|
|
1242
|
+
line: directiveA.line,
|
|
1243
|
+
message: `${category} conflict: "${directiveA.label}" in ${fileA} vs "${directiveB.label}" in ${fileB}`,
|
|
1244
|
+
suggestion: `Align on one ${category} across all context files`,
|
|
1245
|
+
detail: `${fileA}:${directiveA.line} says "${directiveA.text}" but ${fileB}:${directiveB.line} says "${directiveB.text}"`
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
return issues;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// src/core/checks/frontmatter.ts
|
|
1257
|
+
function parseFrontmatter(content) {
|
|
1258
|
+
const lines = content.split("\n");
|
|
1259
|
+
if (lines[0]?.trim() !== "---") {
|
|
1260
|
+
return { found: false, fields: {}, endLine: 0 };
|
|
1261
|
+
}
|
|
1262
|
+
const fields = {};
|
|
1263
|
+
let endLine = 0;
|
|
1264
|
+
for (let i = 1; i < lines.length; i++) {
|
|
1265
|
+
const line = lines[i].trim();
|
|
1266
|
+
if (line === "---") {
|
|
1267
|
+
endLine = i + 1;
|
|
1268
|
+
break;
|
|
1269
|
+
}
|
|
1270
|
+
const match = line.match(/^(\w+)\s*:\s*(.*)$/);
|
|
1271
|
+
if (match) {
|
|
1272
|
+
fields[match[1]] = match[2].trim();
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
if (endLine === 0) {
|
|
1276
|
+
return { found: true, fields, endLine: lines.length };
|
|
1277
|
+
}
|
|
1278
|
+
return { found: true, fields, endLine };
|
|
1279
|
+
}
|
|
1280
|
+
function isCursorMdc(file) {
|
|
1281
|
+
return file.relativePath.endsWith(".mdc");
|
|
1282
|
+
}
|
|
1283
|
+
function isCopilotInstructions(file) {
|
|
1284
|
+
return file.relativePath.includes(".github/instructions/") && file.relativePath.endsWith(".md");
|
|
1285
|
+
}
|
|
1286
|
+
function isWindsurfRule(file) {
|
|
1287
|
+
return file.relativePath.includes(".windsurf/rules/") && file.relativePath.endsWith(".md");
|
|
1288
|
+
}
|
|
1289
|
+
var VALID_WINDSURF_TRIGGERS = ["always_on", "glob", "manual", "model"];
|
|
1290
|
+
async function checkFrontmatter(file, _projectRoot) {
|
|
1291
|
+
const issues = [];
|
|
1292
|
+
if (isCursorMdc(file)) {
|
|
1293
|
+
issues.push(...validateCursorMdc(file));
|
|
1294
|
+
} else if (isCopilotInstructions(file)) {
|
|
1295
|
+
issues.push(...validateCopilotInstructions(file));
|
|
1296
|
+
} else if (isWindsurfRule(file)) {
|
|
1297
|
+
issues.push(...validateWindsurfRule(file));
|
|
1298
|
+
}
|
|
1299
|
+
return issues;
|
|
1300
|
+
}
|
|
1301
|
+
function validateCursorMdc(file) {
|
|
1302
|
+
const issues = [];
|
|
1303
|
+
const fm = parseFrontmatter(file.content);
|
|
1304
|
+
if (!fm.found) {
|
|
1305
|
+
issues.push({
|
|
1306
|
+
severity: "warning",
|
|
1307
|
+
check: "frontmatter",
|
|
1308
|
+
line: 1,
|
|
1309
|
+
message: "Cursor .mdc file is missing frontmatter",
|
|
1310
|
+
suggestion: "Add YAML frontmatter with description, globs, and alwaysApply fields"
|
|
1311
|
+
});
|
|
1312
|
+
return issues;
|
|
1313
|
+
}
|
|
1314
|
+
if (!fm.fields["description"]) {
|
|
1315
|
+
issues.push({
|
|
1316
|
+
severity: "warning",
|
|
1317
|
+
check: "frontmatter",
|
|
1318
|
+
line: 1,
|
|
1319
|
+
message: 'Missing "description" field in Cursor .mdc frontmatter',
|
|
1320
|
+
suggestion: "Add a description so Cursor knows when to apply this rule"
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
if (!("alwaysApply" in fm.fields) && !("globs" in fm.fields)) {
|
|
1324
|
+
issues.push({
|
|
1325
|
+
severity: "info",
|
|
1326
|
+
check: "frontmatter",
|
|
1327
|
+
line: 1,
|
|
1328
|
+
message: 'No "alwaysApply" or "globs" field \u2014 rule may not be applied automatically',
|
|
1329
|
+
suggestion: "Set alwaysApply: true or specify globs for targeted activation"
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
if ("alwaysApply" in fm.fields) {
|
|
1333
|
+
const val = fm.fields["alwaysApply"].toLowerCase();
|
|
1334
|
+
if (!["true", "false"].includes(val)) {
|
|
1335
|
+
issues.push({
|
|
1336
|
+
severity: "error",
|
|
1337
|
+
check: "frontmatter",
|
|
1338
|
+
line: 1,
|
|
1339
|
+
message: `Invalid alwaysApply value: "${fm.fields["alwaysApply"]}"`,
|
|
1340
|
+
suggestion: "alwaysApply must be true or false"
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
if ("globs" in fm.fields) {
|
|
1345
|
+
const val = fm.fields["globs"];
|
|
1346
|
+
if (val && !val.startsWith("[") && !val.startsWith('"') && !val.includes("*") && !val.includes("/")) {
|
|
1347
|
+
issues.push({
|
|
1348
|
+
severity: "warning",
|
|
1349
|
+
check: "frontmatter",
|
|
1350
|
+
line: 1,
|
|
1351
|
+
message: `Possibly invalid globs value: "${val}"`,
|
|
1352
|
+
suggestion: 'globs should be a glob pattern like "src/**/*.ts" or an array like ["*.ts", "*.tsx"]'
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
return issues;
|
|
1357
|
+
}
|
|
1358
|
+
function validateCopilotInstructions(file) {
|
|
1359
|
+
const issues = [];
|
|
1360
|
+
const fm = parseFrontmatter(file.content);
|
|
1361
|
+
if (!fm.found) {
|
|
1362
|
+
issues.push({
|
|
1363
|
+
severity: "info",
|
|
1364
|
+
check: "frontmatter",
|
|
1365
|
+
line: 1,
|
|
1366
|
+
message: "Copilot instructions file has no frontmatter",
|
|
1367
|
+
suggestion: "Add applyTo frontmatter to target specific file patterns"
|
|
1368
|
+
});
|
|
1369
|
+
return issues;
|
|
1370
|
+
}
|
|
1371
|
+
if (!fm.fields["applyTo"]) {
|
|
1372
|
+
issues.push({
|
|
1373
|
+
severity: "warning",
|
|
1374
|
+
check: "frontmatter",
|
|
1375
|
+
line: 1,
|
|
1376
|
+
message: 'Missing "applyTo" field in Copilot instructions frontmatter',
|
|
1377
|
+
suggestion: 'Add applyTo to specify which files this instruction applies to (e.g., applyTo: "**/*.ts")'
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
return issues;
|
|
1381
|
+
}
|
|
1382
|
+
function validateWindsurfRule(file) {
|
|
1383
|
+
const issues = [];
|
|
1384
|
+
const fm = parseFrontmatter(file.content);
|
|
1385
|
+
if (!fm.found) {
|
|
1386
|
+
issues.push({
|
|
1387
|
+
severity: "info",
|
|
1388
|
+
check: "frontmatter",
|
|
1389
|
+
line: 1,
|
|
1390
|
+
message: "Windsurf rule file has no frontmatter",
|
|
1391
|
+
suggestion: "Add YAML frontmatter with a trigger field (always_on, glob, manual, model)"
|
|
1392
|
+
});
|
|
1393
|
+
return issues;
|
|
1394
|
+
}
|
|
1395
|
+
if (!fm.fields["trigger"]) {
|
|
1396
|
+
issues.push({
|
|
1397
|
+
severity: "warning",
|
|
1398
|
+
check: "frontmatter",
|
|
1399
|
+
line: 1,
|
|
1400
|
+
message: 'Missing "trigger" field in Windsurf rule frontmatter',
|
|
1401
|
+
suggestion: `Set trigger to one of: ${VALID_WINDSURF_TRIGGERS.join(", ")}`
|
|
1402
|
+
});
|
|
1403
|
+
} else {
|
|
1404
|
+
const trigger = fm.fields["trigger"].replace(/['"]/g, "");
|
|
1405
|
+
if (!VALID_WINDSURF_TRIGGERS.includes(trigger)) {
|
|
1406
|
+
issues.push({
|
|
1407
|
+
severity: "error",
|
|
1408
|
+
check: "frontmatter",
|
|
1409
|
+
line: 1,
|
|
1410
|
+
message: `Invalid trigger value: "${trigger}"`,
|
|
1411
|
+
suggestion: `Valid triggers: ${VALID_WINDSURF_TRIGGERS.join(", ")}`
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
return issues;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
926
1418
|
// src/core/reporter.ts
|
|
927
1419
|
import chalk from "chalk";
|
|
928
1420
|
function formatText(result, verbose = false) {
|
|
@@ -981,6 +1473,9 @@ function formatTokenReport(result) {
|
|
|
981
1473
|
const lines = [];
|
|
982
1474
|
lines.push("");
|
|
983
1475
|
lines.push(chalk.bold("Token Usage Report"));
|
|
1476
|
+
lines.push(
|
|
1477
|
+
chalk.dim(" (counts use GPT-4 cl100k_base tokenizer \u2014 Claude counts may vary slightly)")
|
|
1478
|
+
);
|
|
984
1479
|
lines.push("");
|
|
985
1480
|
const maxPathLen = Math.max(...result.files.map((f) => f.path.length), 4);
|
|
986
1481
|
lines.push(
|
|
@@ -1007,6 +1502,100 @@ function formatTokenReport(result) {
|
|
|
1007
1502
|
lines.push("");
|
|
1008
1503
|
return lines.join("\n");
|
|
1009
1504
|
}
|
|
1505
|
+
function formatSarif(result) {
|
|
1506
|
+
const severityToLevel = {
|
|
1507
|
+
error: "error",
|
|
1508
|
+
warning: "warning",
|
|
1509
|
+
info: "note"
|
|
1510
|
+
};
|
|
1511
|
+
const results = [];
|
|
1512
|
+
for (const file of result.files) {
|
|
1513
|
+
for (const issue of file.issues) {
|
|
1514
|
+
const sarifResult = {
|
|
1515
|
+
ruleId: `ctxlint/${issue.check}`,
|
|
1516
|
+
level: severityToLevel[issue.severity] || "note",
|
|
1517
|
+
message: {
|
|
1518
|
+
text: issue.message + (issue.suggestion ? ` (${issue.suggestion})` : "")
|
|
1519
|
+
},
|
|
1520
|
+
locations: [
|
|
1521
|
+
{
|
|
1522
|
+
physicalLocation: {
|
|
1523
|
+
artifactLocation: {
|
|
1524
|
+
uri: file.path,
|
|
1525
|
+
uriBaseId: "%SRCROOT%"
|
|
1526
|
+
},
|
|
1527
|
+
region: {
|
|
1528
|
+
startLine: Math.max(issue.line, 1)
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
]
|
|
1533
|
+
};
|
|
1534
|
+
if (issue.detail) {
|
|
1535
|
+
sarifResult.message.text += `
|
|
1536
|
+
${issue.detail}`;
|
|
1537
|
+
}
|
|
1538
|
+
results.push(sarifResult);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
const sarif = {
|
|
1542
|
+
version: "2.1.0",
|
|
1543
|
+
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
|
|
1544
|
+
runs: [
|
|
1545
|
+
{
|
|
1546
|
+
tool: {
|
|
1547
|
+
driver: {
|
|
1548
|
+
name: "ctxlint",
|
|
1549
|
+
version: result.version,
|
|
1550
|
+
informationUri: "https://github.com/yawlabs/ctxlint",
|
|
1551
|
+
rules: buildRuleDescriptors()
|
|
1552
|
+
}
|
|
1553
|
+
},
|
|
1554
|
+
results
|
|
1555
|
+
}
|
|
1556
|
+
]
|
|
1557
|
+
};
|
|
1558
|
+
return JSON.stringify(sarif, null, 2);
|
|
1559
|
+
}
|
|
1560
|
+
function buildRuleDescriptors() {
|
|
1561
|
+
return [
|
|
1562
|
+
{
|
|
1563
|
+
id: "ctxlint/paths",
|
|
1564
|
+
shortDescription: { text: "File path does not exist in project" },
|
|
1565
|
+
helpUri: "https://github.com/yawlabs/ctxlint#what-it-checks"
|
|
1566
|
+
},
|
|
1567
|
+
{
|
|
1568
|
+
id: "ctxlint/commands",
|
|
1569
|
+
shortDescription: { text: "Command does not match project scripts" },
|
|
1570
|
+
helpUri: "https://github.com/yawlabs/ctxlint#what-it-checks"
|
|
1571
|
+
},
|
|
1572
|
+
{
|
|
1573
|
+
id: "ctxlint/staleness",
|
|
1574
|
+
shortDescription: { text: "Context file is stale relative to referenced code" },
|
|
1575
|
+
helpUri: "https://github.com/yawlabs/ctxlint#what-it-checks"
|
|
1576
|
+
},
|
|
1577
|
+
{
|
|
1578
|
+
id: "ctxlint/tokens",
|
|
1579
|
+
shortDescription: { text: "Context file token usage" },
|
|
1580
|
+
helpUri: "https://github.com/yawlabs/ctxlint#what-it-checks"
|
|
1581
|
+
},
|
|
1582
|
+
{
|
|
1583
|
+
id: "ctxlint/redundancy",
|
|
1584
|
+
shortDescription: { text: "Content is redundant or inferable" },
|
|
1585
|
+
helpUri: "https://github.com/yawlabs/ctxlint#what-it-checks"
|
|
1586
|
+
},
|
|
1587
|
+
{
|
|
1588
|
+
id: "ctxlint/contradictions",
|
|
1589
|
+
shortDescription: { text: "Conflicting directives across context files" },
|
|
1590
|
+
helpUri: "https://github.com/yawlabs/ctxlint#what-it-checks"
|
|
1591
|
+
},
|
|
1592
|
+
{
|
|
1593
|
+
id: "ctxlint/frontmatter",
|
|
1594
|
+
shortDescription: { text: "Invalid or missing frontmatter" },
|
|
1595
|
+
helpUri: "https://github.com/yawlabs/ctxlint#what-it-checks"
|
|
1596
|
+
}
|
|
1597
|
+
];
|
|
1598
|
+
}
|
|
1010
1599
|
function formatIssue(issue) {
|
|
1011
1600
|
const icon = issue.severity === "error" ? chalk.red("\u2717") : issue.severity === "warning" ? chalk.yellow("\u26A0") : chalk.blue("\u2139");
|
|
1012
1601
|
const lineRef = issue.line > 0 ? `Line ${issue.line}: ` : "";
|
|
@@ -1086,23 +1675,32 @@ import * as path8 from "path";
|
|
|
1086
1675
|
|
|
1087
1676
|
// src/version.ts
|
|
1088
1677
|
function loadVersion() {
|
|
1089
|
-
if (true) return "0.
|
|
1090
|
-
const
|
|
1678
|
+
if (true) return "0.3.0";
|
|
1679
|
+
const fs7 = __require("fs");
|
|
1091
1680
|
const path9 = __require("path");
|
|
1092
1681
|
const pkgPath = path9.resolve(__dirname, "../package.json");
|
|
1093
|
-
const pkg = JSON.parse(
|
|
1682
|
+
const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
|
|
1094
1683
|
return pkg.version;
|
|
1095
1684
|
}
|
|
1096
1685
|
var VERSION = loadVersion();
|
|
1097
1686
|
|
|
1098
1687
|
// src/index.ts
|
|
1099
|
-
var ALL_CHECKS = [
|
|
1688
|
+
var ALL_CHECKS = [
|
|
1689
|
+
"paths",
|
|
1690
|
+
"commands",
|
|
1691
|
+
"staleness",
|
|
1692
|
+
"tokens",
|
|
1693
|
+
"redundancy",
|
|
1694
|
+
"contradictions",
|
|
1695
|
+
"frontmatter"
|
|
1696
|
+
];
|
|
1100
1697
|
var program = new Command();
|
|
1101
1698
|
program.name("ctxlint").description(
|
|
1102
1699
|
"Lint your AI agent context files (CLAUDE.md, AGENTS.md, etc.) against your actual codebase"
|
|
1103
|
-
).version(VERSION).argument("[path]", "Project directory to scan", ".").option("--strict", "Exit code 1 on any warning or error (for CI)", false).option("--checks <checks>", "Comma-separated list of checks to run", "").option("--format <format>", "Output format: text or
|
|
1700
|
+
).version(VERSION).argument("[path]", "Project directory to scan", ".").option("--strict", "Exit code 1 on any warning or error (for CI)", false).option("--checks <checks>", "Comma-separated list of checks to run", "").option("--format <format>", "Output format: text, json, or sarif", "text").option("--tokens", "Show token breakdown per file", false).option("--verbose", "Show passing checks too", false).option("--fix", "Auto-fix broken paths using git history and fuzzy matching", false).option("--ignore <checks>", "Comma-separated list of checks to ignore", "").option("--quiet", "Suppress all output except errors (exit code only)", false).option("--config <path>", "Path to config file (default: .ctxlintrc in project root)").option("--depth <n>", "Max subdirectory depth to scan (default: 2)", "2").action(async (projectPath, opts) => {
|
|
1104
1701
|
const resolvedPath = path8.resolve(projectPath);
|
|
1105
|
-
const
|
|
1702
|
+
const configPath = opts.config ? path8.resolve(opts.config) : void 0;
|
|
1703
|
+
const config = configPath ? loadConfigFromPath(configPath) : loadConfig(resolvedPath);
|
|
1106
1704
|
const options = {
|
|
1107
1705
|
projectPath: resolvedPath,
|
|
1108
1706
|
checks: opts.checks ? opts.checks.split(",").map((c) => c.trim()) : config?.checks || ALL_CHECKS,
|
|
@@ -1111,29 +1709,46 @@ program.name("ctxlint").description(
|
|
|
1111
1709
|
verbose: opts.verbose,
|
|
1112
1710
|
fix: opts.fix,
|
|
1113
1711
|
ignore: opts.ignore ? opts.ignore.split(",").map((c) => c.trim()) : config?.ignore || [],
|
|
1114
|
-
tokensOnly: opts.tokens
|
|
1712
|
+
tokensOnly: opts.tokens,
|
|
1713
|
+
quiet: opts.quiet,
|
|
1714
|
+
depth: parseInt(opts.depth, 10) || 2
|
|
1115
1715
|
};
|
|
1116
1716
|
if (config?.tokenThresholds) {
|
|
1117
1717
|
setTokenThresholds(config.tokenThresholds);
|
|
1118
1718
|
}
|
|
1119
1719
|
const activeChecks = options.checks.filter((c) => !options.ignore.includes(c));
|
|
1120
|
-
const spinner = options.format === "text" ? ora("Scanning for context files...").start() : void 0;
|
|
1720
|
+
const spinner = options.format === "text" && !options.quiet ? ora("Scanning for context files...").start() : void 0;
|
|
1121
1721
|
try {
|
|
1122
|
-
const discovered = await scanForContextFiles(options.projectPath
|
|
1722
|
+
const discovered = await scanForContextFiles(options.projectPath, {
|
|
1723
|
+
depth: options.depth,
|
|
1724
|
+
extraPatterns: config?.contextFiles
|
|
1725
|
+
});
|
|
1123
1726
|
if (discovered.length === 0) {
|
|
1124
1727
|
spinner?.stop();
|
|
1125
|
-
if (options.
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1728
|
+
if (!options.quiet) {
|
|
1729
|
+
if (options.format === "json") {
|
|
1730
|
+
console.log(
|
|
1731
|
+
JSON.stringify({
|
|
1732
|
+
version: VERSION,
|
|
1733
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1734
|
+
projectRoot: options.projectPath,
|
|
1735
|
+
files: [],
|
|
1736
|
+
summary: { errors: 0, warnings: 0, info: 0, totalTokens: 0, estimatedWaste: 0 }
|
|
1737
|
+
})
|
|
1738
|
+
);
|
|
1739
|
+
} else if (options.format === "sarif") {
|
|
1740
|
+
console.log(
|
|
1741
|
+
formatSarif({
|
|
1742
|
+
version: VERSION,
|
|
1743
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1744
|
+
projectRoot: options.projectPath,
|
|
1745
|
+
files: [],
|
|
1746
|
+
summary: { errors: 0, warnings: 0, info: 0, totalTokens: 0, estimatedWaste: 0 }
|
|
1747
|
+
})
|
|
1748
|
+
);
|
|
1749
|
+
} else {
|
|
1750
|
+
console.log("\nNo context files found.\n");
|
|
1751
|
+
}
|
|
1137
1752
|
}
|
|
1138
1753
|
process.exit(0);
|
|
1139
1754
|
}
|
|
@@ -1159,6 +1774,9 @@ program.name("ctxlint").description(
|
|
|
1159
1774
|
if (activeChecks.includes("redundancy")) {
|
|
1160
1775
|
issues.push(...await checkRedundancy(file, options.projectPath));
|
|
1161
1776
|
}
|
|
1777
|
+
if (activeChecks.includes("frontmatter")) {
|
|
1778
|
+
issues.push(...await checkFrontmatter(file, options.projectPath));
|
|
1779
|
+
}
|
|
1162
1780
|
fileResults.push({
|
|
1163
1781
|
path: file.relativePath,
|
|
1164
1782
|
isSymlink: file.isSymlink,
|
|
@@ -1182,6 +1800,12 @@ program.name("ctxlint").description(
|
|
|
1182
1800
|
fileResults[0].issues.push(...dupIssues);
|
|
1183
1801
|
}
|
|
1184
1802
|
}
|
|
1803
|
+
if (activeChecks.includes("contradictions")) {
|
|
1804
|
+
const contradictionIssues = checkContradictions(parsed);
|
|
1805
|
+
if (contradictionIssues.length > 0 && fileResults.length > 0) {
|
|
1806
|
+
fileResults[0].issues.push(...contradictionIssues);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1185
1809
|
let estimatedWaste = 0;
|
|
1186
1810
|
for (const fr of fileResults) {
|
|
1187
1811
|
for (const issue of fr.issues) {
|
|
@@ -1218,7 +1842,7 @@ program.name("ctxlint").description(
|
|
|
1218
1842
|
spinner?.stop();
|
|
1219
1843
|
if (options.fix) {
|
|
1220
1844
|
const fixSummary = applyFixes(result);
|
|
1221
|
-
if (fixSummary.totalFixes > 0) {
|
|
1845
|
+
if (fixSummary.totalFixes > 0 && !options.quiet) {
|
|
1222
1846
|
console.log(
|
|
1223
1847
|
`
|
|
1224
1848
|
Fixed ${fixSummary.totalFixes} issue${fixSummary.totalFixes !== 1 ? "s" : ""} in ${fixSummary.filesModified.length} file${fixSummary.filesModified.length !== 1 ? "s" : ""}.
|
|
@@ -1226,12 +1850,16 @@ Fixed ${fixSummary.totalFixes} issue${fixSummary.totalFixes !== 1 ? "s" : ""} in
|
|
|
1226
1850
|
);
|
|
1227
1851
|
}
|
|
1228
1852
|
}
|
|
1229
|
-
if (options.
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1853
|
+
if (!options.quiet) {
|
|
1854
|
+
if (options.tokensOnly) {
|
|
1855
|
+
console.log(formatTokenReport(result));
|
|
1856
|
+
} else if (options.format === "json") {
|
|
1857
|
+
console.log(formatJson(result));
|
|
1858
|
+
} else if (options.format === "sarif") {
|
|
1859
|
+
console.log(formatSarif(result));
|
|
1860
|
+
} else {
|
|
1861
|
+
console.log(formatText(result, options.verbose));
|
|
1862
|
+
}
|
|
1235
1863
|
}
|
|
1236
1864
|
if (options.strict && (result.summary.errors > 0 || result.summary.warnings > 0)) {
|
|
1237
1865
|
process.exit(1);
|
|
@@ -1248,7 +1876,6 @@ Fixed ${fixSummary.totalFixes} issue${fixSummary.totalFixes !== 1 ? "s" : ""} in
|
|
|
1248
1876
|
}
|
|
1249
1877
|
});
|
|
1250
1878
|
program.command("init").description("Set up a git pre-commit hook that runs ctxlint --strict").action(async () => {
|
|
1251
|
-
const fs6 = await import("fs");
|
|
1252
1879
|
const hooksDir = path8.resolve(".git", "hooks");
|
|
1253
1880
|
if (!fs6.existsSync(path8.resolve(".git"))) {
|
|
1254
1881
|
console.error('Error: not a git repository. Run "git init" first.');
|
|
@@ -1277,3 +1904,12 @@ npx @yawlabs/ctxlint --strict
|
|
|
1277
1904
|
console.log("ctxlint will now run automatically before each commit.");
|
|
1278
1905
|
});
|
|
1279
1906
|
program.parse();
|
|
1907
|
+
function loadConfigFromPath(configPath) {
|
|
1908
|
+
try {
|
|
1909
|
+
const content = fs6.readFileSync(configPath, "utf-8");
|
|
1910
|
+
return JSON.parse(content);
|
|
1911
|
+
} catch {
|
|
1912
|
+
console.error(`Error: could not load config from ${configPath}`);
|
|
1913
|
+
process.exit(2);
|
|
1914
|
+
}
|
|
1915
|
+
}
|