@konvert7/klint 0.11.0 → 0.12.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/cli.ts +43 -3
- package/core/config.schema.ts +2 -2
- package/core/runner.ts +53 -11
- package/package.json +1 -1
- package/tools/prepare-native-release.ts +17 -11
package/cli.ts
CHANGED
|
@@ -13,7 +13,7 @@ import * as clack from "@clack/prompts";
|
|
|
13
13
|
import { parse as parseYaml } from "yaml";
|
|
14
14
|
import { applyFixes } from "./core/fixer";
|
|
15
15
|
import { resolveNativePackageBinary } from "./core/native-binary";
|
|
16
|
-
import { runKlint } from "./core/runner";
|
|
16
|
+
import { type KlintDebugEvent, runKlint } from "./core/runner";
|
|
17
17
|
import type { ArchConfig, KlintConfig, KlintRule, RuleConfigValue } from "./core/types";
|
|
18
18
|
import { BUILT_IN_PLUGINS } from "./plugins/index";
|
|
19
19
|
import { BUILT_IN_RULES } from "./rules/index";
|
|
@@ -40,12 +40,14 @@ export async function main(opts: CliOptions = {}): Promise<void> {
|
|
|
40
40
|
let rulesFile = opts.rulesFile;
|
|
41
41
|
let fix = false;
|
|
42
42
|
let json = false;
|
|
43
|
+
let debug = false;
|
|
43
44
|
|
|
44
45
|
for (let i = 0; i < args.length; i++) {
|
|
45
46
|
if (args[i] === "--config" && args[i + 1]) configDir = resolve(args[++i]);
|
|
46
47
|
else if (args[i] === "--rules" && args[i + 1]) rulesFile = resolve(args[++i]);
|
|
47
48
|
else if (args[i] === "--fix") fix = true;
|
|
48
49
|
else if (args[i] === "--json") json = true;
|
|
50
|
+
else if (args[i] === "--debug" || args[i] === "-debug") debug = true;
|
|
49
51
|
else if (args[i] === "--help" || args[i] === "-h") {
|
|
50
52
|
printHelp();
|
|
51
53
|
process.exit(0);
|
|
@@ -111,7 +113,8 @@ export async function main(opts: CliOptions = {}): Promise<void> {
|
|
|
111
113
|
rules: allRules,
|
|
112
114
|
arch: raw.arch as ArchConfig | undefined,
|
|
113
115
|
},
|
|
114
|
-
customRules
|
|
116
|
+
customRules,
|
|
117
|
+
{ onDebug: debug ? writeDebugEvent : undefined }
|
|
115
118
|
);
|
|
116
119
|
|
|
117
120
|
if (json) {
|
|
@@ -140,7 +143,8 @@ export async function main(opts: CliOptions = {}): Promise<void> {
|
|
|
140
143
|
rules: allRules,
|
|
141
144
|
arch: raw.arch as ArchConfig | undefined,
|
|
142
145
|
},
|
|
143
|
-
customRules
|
|
146
|
+
customRules,
|
|
147
|
+
{ onDebug: debug ? writeDebugEvent : undefined }
|
|
144
148
|
);
|
|
145
149
|
if (current.every((v) => !v.fix)) break;
|
|
146
150
|
}
|
|
@@ -212,6 +216,11 @@ function runRustEngine({
|
|
|
212
216
|
}
|
|
213
217
|
|
|
214
218
|
const command = resolveRustEngineCommand(configDir);
|
|
219
|
+
if (process.argv.includes("--debug") || process.argv.includes("-debug")) {
|
|
220
|
+
process.stderr.write(
|
|
221
|
+
`[klint:debug] rust engine: ${command.bin} ${command.args.join(" ")}\n`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
215
224
|
const result = spawnSync(command.bin, command.args, {
|
|
216
225
|
cwd: import.meta.dir,
|
|
217
226
|
encoding: "utf-8",
|
|
@@ -222,6 +231,36 @@ function runRustEngine({
|
|
|
222
231
|
process.exit(result.status ?? 1);
|
|
223
232
|
}
|
|
224
233
|
|
|
234
|
+
function writeDebugEvent(event: KlintDebugEvent): void {
|
|
235
|
+
switch (event.type) {
|
|
236
|
+
case "walk:start":
|
|
237
|
+
process.stderr.write(`[klint:debug] walk ${event.dir}\n`);
|
|
238
|
+
break;
|
|
239
|
+
case "walk:done":
|
|
240
|
+
process.stderr.write(`[klint:debug] walked ${event.dir} (${event.files} files)\n`);
|
|
241
|
+
break;
|
|
242
|
+
case "files:resolved":
|
|
243
|
+
process.stderr.write(`[klint:debug] resolved ${event.files} files\n`);
|
|
244
|
+
break;
|
|
245
|
+
case "rule:start":
|
|
246
|
+
process.stderr.write(
|
|
247
|
+
`[klint:debug] rule ${event.rule} start (${event.files} files)\n`
|
|
248
|
+
);
|
|
249
|
+
break;
|
|
250
|
+
case "rule:done":
|
|
251
|
+
process.stderr.write(
|
|
252
|
+
`[klint:debug] rule ${event.rule} done (${event.violations} violations)\n`
|
|
253
|
+
);
|
|
254
|
+
break;
|
|
255
|
+
case "arch:start":
|
|
256
|
+
process.stderr.write(`[klint:debug] arch start (${event.files} files)\n`);
|
|
257
|
+
break;
|
|
258
|
+
case "arch:done":
|
|
259
|
+
process.stderr.write(`[klint:debug] arch done (${event.violations} violations)\n`);
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
225
264
|
interface RustEngineCommand {
|
|
226
265
|
bin: string;
|
|
227
266
|
args: string[];
|
|
@@ -396,6 +435,7 @@ function printHelp(): void {
|
|
|
396
435
|
" --rules <file> custom rules file (default: <configDir>/klint.rules.ts if present)",
|
|
397
436
|
" --fix apply auto-fixes for fixable violations in-place",
|
|
398
437
|
" --json emit structured JSON to stdout (for agent/CI consumption)",
|
|
438
|
+
" --debug print file resolution and rule progress to stderr",
|
|
399
439
|
"",
|
|
400
440
|
" install-skill install the rule-authoring skill into agent config directories",
|
|
401
441
|
" --agents <list> comma-separated: claude,opencode,cursor,codex (default: all)",
|
package/core/config.schema.ts
CHANGED
|
@@ -115,7 +115,7 @@ const ArchForbiddenPatternRuleSchema = z
|
|
|
115
115
|
pattern: z
|
|
116
116
|
.string()
|
|
117
117
|
.describe(
|
|
118
|
-
"Literal substring to search for in source. No regex — exact substring match (
|
|
118
|
+
"Literal substring to search for in source. No regex — exact substring match (for example, a console logging call)."
|
|
119
119
|
),
|
|
120
120
|
in: StringOrStringArray.describe(
|
|
121
121
|
"Layer name(s) or glob(s) the pattern is forbidden in. Files outside the scope are not checked."
|
|
@@ -153,7 +153,7 @@ const ArchForbiddenRuleSchema = z
|
|
|
153
153
|
)
|
|
154
154
|
.meta({
|
|
155
155
|
examples: [
|
|
156
|
-
'arch:\n forbidden:\n - pattern: "
|
|
156
|
+
'arch:\n forbidden:\n - pattern: "debugLog("\n in: core\n message: "Leaks into the agent event stream"',
|
|
157
157
|
'arch:\n forbidden:\n - jsx-element: ["button", "input", "label"]\n in: ["src/app/**/*.tsx", "src/components/**/*.tsx", "!src/components/ui/**"]\n message: "Use the design-system primitives in @/components/ui/* instead of raw HTML elements."',
|
|
158
158
|
],
|
|
159
159
|
});
|
package/core/runner.ts
CHANGED
|
@@ -12,23 +12,49 @@ import type {
|
|
|
12
12
|
Violation,
|
|
13
13
|
} from "./types";
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
export type KlintDebugEvent =
|
|
16
|
+
| { type: "walk:start"; dir: string }
|
|
17
|
+
| { type: "walk:done"; dir: string; files: number }
|
|
18
|
+
| { type: "files:resolved"; files: number }
|
|
19
|
+
| { type: "rule:start"; rule: string; files: number }
|
|
20
|
+
| { type: "rule:done"; rule: string; violations: number }
|
|
21
|
+
| { type: "arch:start"; files: number }
|
|
22
|
+
| { type: "arch:done"; violations: number };
|
|
23
|
+
|
|
24
|
+
export interface RunKlintOptions {
|
|
25
|
+
onDebug?: (event: KlintDebugEvent) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function walk(dir: string, root: string, excludes: string[] = []): string[] {
|
|
16
29
|
const out: string[] = [];
|
|
17
30
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
18
31
|
const full = join(dir, entry.name);
|
|
19
|
-
|
|
20
|
-
|
|
32
|
+
const rel = relative(root, full).replaceAll("\\", "/");
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
if (excludes.some((pattern) => matchPattern(rel, pattern))) continue;
|
|
35
|
+
out.push(...walk(full, root, excludes));
|
|
36
|
+
} else if (/\.(tsx?|jsx?|mts|cts)$/.test(entry.name))
|
|
21
37
|
out.push(full.replaceAll("\\", "/"));
|
|
22
38
|
}
|
|
23
39
|
return out;
|
|
24
40
|
}
|
|
25
41
|
|
|
26
|
-
function resolveFiles(
|
|
42
|
+
function resolveFiles(
|
|
43
|
+
include: string[],
|
|
44
|
+
root: string,
|
|
45
|
+
onDebug?: (event: KlintDebugEvent) => void
|
|
46
|
+
): string[] {
|
|
27
47
|
const files = new Set<string>();
|
|
48
|
+
const excludes = include.filter((p) => p.startsWith("!")).map((p) => p.slice(1));
|
|
28
49
|
for (const pattern of include) {
|
|
29
|
-
|
|
50
|
+
if (pattern.startsWith("!")) continue;
|
|
51
|
+
const base = pattern.split("/**")[0].split("/*")[0];
|
|
52
|
+
const dir = resolve(root, base === "**" ? "." : base);
|
|
30
53
|
try {
|
|
31
|
-
|
|
54
|
+
onDebug?.({ type: "walk:start", dir });
|
|
55
|
+
const before = files.size;
|
|
56
|
+
for (const file of walk(dir, root, excludes)) files.add(file);
|
|
57
|
+
onDebug?.({ type: "walk:done", dir, files: files.size - before });
|
|
32
58
|
} catch {
|
|
33
59
|
// directory doesn't exist — skip
|
|
34
60
|
}
|
|
@@ -41,8 +67,17 @@ function matchPattern(relPath: string, pattern: string): boolean {
|
|
|
41
67
|
const p = pattern.replaceAll("\\", "/");
|
|
42
68
|
if (p === "." || p === "**") return true;
|
|
43
69
|
if (p.endsWith("/**")) return norm.startsWith(`${p.slice(0, -3)}/`);
|
|
44
|
-
if (p.
|
|
45
|
-
return
|
|
70
|
+
if (!p.includes("*")) return norm === p || norm.startsWith(`${p}/`);
|
|
71
|
+
return globToRegExp(p).test(norm);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function globToRegExp(pattern: string): RegExp {
|
|
75
|
+
const escaped = pattern
|
|
76
|
+
.replace(/[.+?^${}()|[\]\\]/g, "\\$&")
|
|
77
|
+
.replaceAll("**/", "(?:.*/)?")
|
|
78
|
+
.replaceAll("**", ".*")
|
|
79
|
+
.replaceAll("*", "[^/]*");
|
|
80
|
+
return new RegExp(`^${escaped}$`);
|
|
46
81
|
}
|
|
47
82
|
|
|
48
83
|
function applyPatterns(files: string[], patterns: string[], root: string): string[] {
|
|
@@ -66,7 +101,8 @@ function resolveInclude(value: RuleConfigValue): string[] | undefined {
|
|
|
66
101
|
|
|
67
102
|
export function runKlint(
|
|
68
103
|
config: KlintConfig,
|
|
69
|
-
customRules: Record<string, KlintRule> = {}
|
|
104
|
+
customRules: Record<string, KlintRule> = {},
|
|
105
|
+
options: RunKlintOptions = {}
|
|
70
106
|
): Violation[] {
|
|
71
107
|
clearAstCache();
|
|
72
108
|
|
|
@@ -94,10 +130,11 @@ export function runKlint(
|
|
|
94
130
|
};
|
|
95
131
|
|
|
96
132
|
const allFiles = applyPatterns(
|
|
97
|
-
resolveFiles(config.include, config.root),
|
|
133
|
+
resolveFiles(config.include, config.root, options.onDebug),
|
|
98
134
|
config.include,
|
|
99
135
|
config.root
|
|
100
136
|
);
|
|
137
|
+
options.onDebug?.({ type: "files:resolved", files: allFiles.length });
|
|
101
138
|
const fileContents = new Map(allFiles.map((f) => [f, readFileSync(f, "utf-8")]));
|
|
102
139
|
const violations: Violation[] = [];
|
|
103
140
|
|
|
@@ -112,12 +149,17 @@ export function runKlint(
|
|
|
112
149
|
const files = include ? applyPatterns(allFiles, include, config.root) : allFiles;
|
|
113
150
|
|
|
114
151
|
const batch: Omit<Violation, "severity">[] = [];
|
|
152
|
+
options.onDebug?.({ type: "rule:start", rule: ruleName, files: files.length });
|
|
115
153
|
rule.check({ files, root: config.root, fileContents }, batch);
|
|
154
|
+
options.onDebug?.({ type: "rule:done", rule: ruleName, violations: batch.length });
|
|
116
155
|
for (const v of batch) violations.push({ ...v, rule: ruleName, severity });
|
|
117
156
|
}
|
|
118
157
|
|
|
119
158
|
if (config.arch) {
|
|
120
|
-
|
|
159
|
+
options.onDebug?.({ type: "arch:start", files: allFiles.length });
|
|
160
|
+
const archViolations = runArchRules(config.arch, allFiles, fileContents, config.root);
|
|
161
|
+
options.onDebug?.({ type: "arch:done", violations: archViolations.length });
|
|
162
|
+
violations.push(...archViolations);
|
|
121
163
|
}
|
|
122
164
|
|
|
123
165
|
return violations;
|
package/package.json
CHANGED
|
@@ -49,16 +49,17 @@ try {
|
|
|
49
49
|
fail(`Missing native release binaries:\n${missing.join("\n")}`);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
`
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
52
|
+
const missingMessage =
|
|
53
|
+
missing.length > 0
|
|
54
|
+
? `missing binaries: ${missing.join(", ")}`
|
|
55
|
+
: "missing binaries: none";
|
|
56
|
+
const outputLines = [
|
|
57
|
+
`native release dry-run ok (${options.version})`,
|
|
58
|
+
`output: ${options.outDir}`,
|
|
59
|
+
`native packages: ${nativeResults.length}`,
|
|
60
|
+
missingMessage,
|
|
61
|
+
];
|
|
62
|
+
process.stdout.write(`${outputLines.join("\n")}\n`);
|
|
62
63
|
} finally {
|
|
63
64
|
if (!options.keep) {
|
|
64
65
|
rmSync(options.outDir, { recursive: true, force: true });
|
|
@@ -125,7 +126,12 @@ function validateNativePackage(packageJson: PackageJson): void {
|
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
function readPackageJson(path: string): PackageJson {
|
|
128
|
-
|
|
129
|
+
try {
|
|
130
|
+
return JSON.parse(readFileSync(path, "utf-8")) as PackageJson;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
133
|
+
fail(`Failed to read package JSON at ${path}: ${message}`);
|
|
134
|
+
}
|
|
129
135
|
}
|
|
130
136
|
|
|
131
137
|
function writeJson(path: string, value: unknown): void {
|