@konvert7/klint 0.16.0 → 0.17.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 +154 -19
- package/package.json +1 -1
- package/rules/no-string-match.ts +1 -1
package/cli.ts
CHANGED
|
@@ -18,6 +18,8 @@ import type { ArchConfig, KlintConfig, KlintRule, RuleConfigValue } from "./core
|
|
|
18
18
|
import { BUILT_IN_PLUGINS } from "./plugins/index";
|
|
19
19
|
import { BUILT_IN_RULES } from "./rules/index";
|
|
20
20
|
|
|
21
|
+
const RUST_SUPPORTED_TYPESCRIPT_RULES = new Set(["no-string-match"]);
|
|
22
|
+
|
|
21
23
|
interface CliOptions {
|
|
22
24
|
configDir?: string;
|
|
23
25
|
rulesFile?: string;
|
|
@@ -42,10 +44,12 @@ export async function main(opts: CliOptions = {}): Promise<void> {
|
|
|
42
44
|
let fix = false;
|
|
43
45
|
let json = false;
|
|
44
46
|
let debug = false;
|
|
47
|
+
let engine = process.env.KLINT_ENGINE;
|
|
45
48
|
|
|
46
49
|
for (let i = 0; i < args.length; i++) {
|
|
47
50
|
if (args[i] === "--config" && args[i + 1]) configDir = resolve(args[++i]);
|
|
48
51
|
else if (args[i] === "--rules" && args[i + 1]) rulesFile = resolve(args[++i]);
|
|
52
|
+
else if (args[i] === "--engine" && args[i + 1]) engine = args[++i];
|
|
49
53
|
else if (args[i] === "--fix") fix = true;
|
|
50
54
|
else if (args[i] === "--json") json = true;
|
|
51
55
|
else if (args[i] === "--debug" || args[i] === "-debug") debug = true;
|
|
@@ -85,12 +89,17 @@ export async function main(opts: CliOptions = {}): Promise<void> {
|
|
|
85
89
|
process.exit(1);
|
|
86
90
|
}
|
|
87
91
|
const root = resolve(configDir, raw.root ?? ".");
|
|
88
|
-
const engine = process.env.KLINT_ENGINE;
|
|
89
92
|
|
|
90
93
|
if (engine === "rust") {
|
|
91
94
|
runRustEngine({ configDir, fix, json, raw, rulesFile });
|
|
92
95
|
return;
|
|
93
96
|
}
|
|
97
|
+
if (engine !== undefined && engine !== "ts" && engine !== "compare") {
|
|
98
|
+
process.stderr.write(
|
|
99
|
+
`klint: unknown engine "${engine}" (expected "ts", "rust", or "compare")\n`
|
|
100
|
+
);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
94
103
|
|
|
95
104
|
let customRules: Record<string, KlintRule> = {};
|
|
96
105
|
const defaultRulesPath = resolve(configDir, "klint.rules.ts");
|
|
@@ -118,15 +127,15 @@ export async function main(opts: CliOptions = {}): Promise<void> {
|
|
|
118
127
|
{ onDebug: debug ? writeDebugEvent : undefined }
|
|
119
128
|
);
|
|
120
129
|
|
|
130
|
+
if (engine === "compare") {
|
|
131
|
+
runCompareEngine({ configDir, fix, json, raw, rulesFile, tsViolations: violations });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
121
135
|
if (json) {
|
|
122
|
-
const
|
|
123
|
-
process.stdout.write(
|
|
124
|
-
|
|
125
|
-
violations: violations.map((v) => ({ ...v, fix: v.fix ?? null })),
|
|
126
|
-
summary: { errors: errors.length, warnings: violations.length - errors.length },
|
|
127
|
-
})
|
|
128
|
-
);
|
|
129
|
-
process.exit(errors.length > 0 ? 2 : 0);
|
|
136
|
+
const tsResult = toJsonCliResult(violations);
|
|
137
|
+
process.stdout.write(tsResult.stdout);
|
|
138
|
+
process.exit(tsResult.status);
|
|
130
139
|
}
|
|
131
140
|
|
|
132
141
|
if (fix) {
|
|
@@ -240,6 +249,83 @@ function runRustEngine({
|
|
|
240
249
|
process.exit(result.status ?? 1);
|
|
241
250
|
}
|
|
242
251
|
|
|
252
|
+
function runCompareEngine({
|
|
253
|
+
configDir,
|
|
254
|
+
fix,
|
|
255
|
+
json,
|
|
256
|
+
raw,
|
|
257
|
+
rulesFile,
|
|
258
|
+
tsViolations,
|
|
259
|
+
}: {
|
|
260
|
+
configDir: string;
|
|
261
|
+
fix: boolean;
|
|
262
|
+
json: boolean;
|
|
263
|
+
raw: {
|
|
264
|
+
plugins?: string[];
|
|
265
|
+
rules?: Record<string, RuleConfigValue>;
|
|
266
|
+
arch?: unknown;
|
|
267
|
+
};
|
|
268
|
+
rulesFile?: string;
|
|
269
|
+
tsViolations: ReturnType<typeof runKlint>;
|
|
270
|
+
}): void {
|
|
271
|
+
const unsupportedReason = rustEngineUnsupportedReason({
|
|
272
|
+
fix,
|
|
273
|
+
json,
|
|
274
|
+
raw,
|
|
275
|
+
rulesFile,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
if (unsupportedReason) {
|
|
279
|
+
process.stderr.write(`klint: ${unsupportedReason}\n`);
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const tsResult = toJsonCliResult(tsViolations);
|
|
284
|
+
const rustCommand = resolveRustEngineCommand(configDir);
|
|
285
|
+
const rustResult = spawnSync(rustCommand.bin, rustCommand.args, {
|
|
286
|
+
cwd: import.meta.dir,
|
|
287
|
+
encoding: "utf-8",
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const rustStatus = rustResult.status ?? 1;
|
|
291
|
+
if (rustResult.stderr) process.stderr.write(rustResult.stderr);
|
|
292
|
+
|
|
293
|
+
const tsPayload = parseJsonPayload(tsResult.stdout);
|
|
294
|
+
const rustPayload = parseJsonPayload(rustResult.stdout);
|
|
295
|
+
|
|
296
|
+
if (
|
|
297
|
+
rustStatus !== tsResult.status ||
|
|
298
|
+
tsPayload === undefined ||
|
|
299
|
+
rustPayload === undefined ||
|
|
300
|
+
!jsonPayloadEquals(tsPayload, rustPayload)
|
|
301
|
+
) {
|
|
302
|
+
const mismatchLines = [
|
|
303
|
+
"klint: compare engine mismatch",
|
|
304
|
+
`TypeScript exit: ${tsResult.status}`,
|
|
305
|
+
`Rust exit: ${rustStatus}`,
|
|
306
|
+
];
|
|
307
|
+
process.stderr.write(`${mismatchLines.join("\n")}\n`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
process.stdout.write(tsResult.stdout);
|
|
312
|
+
process.exit(tsResult.status);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function toJsonCliResult(violations: ReturnType<typeof runKlint>): {
|
|
316
|
+
stdout: string;
|
|
317
|
+
status: number;
|
|
318
|
+
} {
|
|
319
|
+
const errors = violations.filter((v) => v.severity === "error");
|
|
320
|
+
return {
|
|
321
|
+
stdout: JSON.stringify({
|
|
322
|
+
violations: violations.map((v) => ({ ...v, fix: v.fix ?? null })),
|
|
323
|
+
summary: { errors: errors.length, warnings: violations.length - errors.length },
|
|
324
|
+
}),
|
|
325
|
+
status: errors.length > 0 ? 2 : 0,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
243
329
|
function writeDebugEvent(event: KlintDebugEvent): void {
|
|
244
330
|
switch (event.type) {
|
|
245
331
|
case "walk:start":
|
|
@@ -282,27 +368,31 @@ function resolveRustEngineCommand(configDir: string): RustEngineCommand {
|
|
|
282
368
|
return { bin: explicitBin, args };
|
|
283
369
|
}
|
|
284
370
|
|
|
285
|
-
const nativePackageBin = resolveNativePackageBinary({ packageRoot: import.meta.dir });
|
|
286
|
-
if (nativePackageBin) {
|
|
287
|
-
return { bin: nativePackageBin, args };
|
|
288
|
-
}
|
|
289
|
-
|
|
290
371
|
const localBin = join(
|
|
291
372
|
import.meta.dir,
|
|
292
373
|
"target",
|
|
293
374
|
"debug",
|
|
294
375
|
process.platform === "win32" ? "klint-rs.exe" : "klint-rs"
|
|
295
376
|
);
|
|
296
|
-
if (existsSync(localBin)) {
|
|
377
|
+
if (isSourceCheckout() && existsSync(localBin)) {
|
|
297
378
|
return { bin: localBin, args };
|
|
298
379
|
}
|
|
299
380
|
|
|
381
|
+
const nativePackageBin = resolveNativePackageBinary({ packageRoot: import.meta.dir });
|
|
382
|
+
if (nativePackageBin) {
|
|
383
|
+
return { bin: nativePackageBin, args };
|
|
384
|
+
}
|
|
385
|
+
|
|
300
386
|
return {
|
|
301
387
|
bin: "cargo",
|
|
302
388
|
args: ["run", "--quiet", "-p", "klint-rs", "--", ...args],
|
|
303
389
|
};
|
|
304
390
|
}
|
|
305
391
|
|
|
392
|
+
function isSourceCheckout(): boolean {
|
|
393
|
+
return existsSync(join(import.meta.dir, "crates", "klint-rs", "Cargo.toml"));
|
|
394
|
+
}
|
|
395
|
+
|
|
306
396
|
function rustEngineUnsupportedReason({
|
|
307
397
|
fix,
|
|
308
398
|
json,
|
|
@@ -321,14 +411,58 @@ function rustEngineUnsupportedReason({
|
|
|
321
411
|
if (!json) return "KLINT_ENGINE=rust currently requires --json";
|
|
322
412
|
if (fix) return "KLINT_ENGINE=rust does not support --fix";
|
|
323
413
|
if (rulesFile) return "KLINT_ENGINE=rust does not support --rules";
|
|
324
|
-
if (!raw.arch) return "KLINT_ENGINE=rust requires an arch config";
|
|
325
414
|
if ((raw.plugins?.length ?? 0) > 0) return "KLINT_ENGINE=rust does not support plugins";
|
|
326
|
-
|
|
327
|
-
|
|
415
|
+
const activeSupportedRules = Object.entries(raw.rules ?? {}).filter(
|
|
416
|
+
([name, value]) =>
|
|
417
|
+
ruleConfigSeverity(value) !== "off" && RUST_SUPPORTED_TYPESCRIPT_RULES.has(name)
|
|
418
|
+
);
|
|
419
|
+
const unsupportedRules = Object.entries(raw.rules ?? {})
|
|
420
|
+
.filter(
|
|
421
|
+
([name, value]) =>
|
|
422
|
+
ruleConfigSeverity(value) !== "off" && !RUST_SUPPORTED_TYPESCRIPT_RULES.has(name)
|
|
423
|
+
)
|
|
424
|
+
.map(([name]) => name);
|
|
425
|
+
if (!raw.arch && activeSupportedRules.length === 0 && unsupportedRules.length === 0) {
|
|
426
|
+
return "KLINT_ENGINE=rust requires an arch config or supported TypeScript rule";
|
|
427
|
+
}
|
|
428
|
+
if (unsupportedRules.length > 0) {
|
|
429
|
+
return [
|
|
430
|
+
"Rust engine currently supports arch rules and selected TypeScript rules only",
|
|
431
|
+
"",
|
|
432
|
+
"Unsupported TypeScript rules:",
|
|
433
|
+
...unsupportedRules.map((rule) => `- ${rule}`),
|
|
434
|
+
].join("\n");
|
|
328
435
|
}
|
|
329
436
|
return undefined;
|
|
330
437
|
}
|
|
331
438
|
|
|
439
|
+
function ruleConfigSeverity(value: RuleConfigValue): string | undefined {
|
|
440
|
+
if (typeof value === "string") return value;
|
|
441
|
+
return value.severity;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function parseJsonPayload(value: string): unknown {
|
|
445
|
+
try {
|
|
446
|
+
return JSON.parse(value);
|
|
447
|
+
} catch {
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function jsonPayloadEquals(a: unknown, b: unknown): boolean {
|
|
453
|
+
return JSON.stringify(canonicalJson(a)) === JSON.stringify(canonicalJson(b));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function canonicalJson(value: unknown): unknown {
|
|
457
|
+
if (Array.isArray(value)) return value.map(canonicalJson);
|
|
458
|
+
if (!value || typeof value !== "object") return value;
|
|
459
|
+
return Object.fromEntries(
|
|
460
|
+
Object.entries(value)
|
|
461
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
462
|
+
.map(([key, entry]) => [key, canonicalJson(entry)])
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
332
466
|
const AGENT_TARGETS = [
|
|
333
467
|
{ value: "claude", label: "Claude Code" },
|
|
334
468
|
{ value: "opencode", label: "opencode" },
|
|
@@ -437,11 +571,12 @@ function printHelp(): void {
|
|
|
437
571
|
[
|
|
438
572
|
"klint — agent harness for TypeScript architecture rules",
|
|
439
573
|
"",
|
|
440
|
-
"Usage: klint [--config <dir>] [--rules <file>] [--fix] [--json]",
|
|
574
|
+
"Usage: klint [--config <dir>] [--rules <file>] [--engine ts|rust|compare] [--fix] [--json]",
|
|
441
575
|
" klint install-skill [--agents <list>] [--symlink | --copy]",
|
|
442
576
|
"",
|
|
443
577
|
" --config <dir> directory containing klint.yaml or klint.config.json (default: cwd)",
|
|
444
578
|
" --rules <file> custom rules file (default: <configDir>/klint.rules.ts if present)",
|
|
579
|
+
" --engine <name> engine to use: ts (default), rust, or compare (experimental, requires --json)",
|
|
445
580
|
" --fix apply auto-fixes for fixable violations in-place",
|
|
446
581
|
" --json emit structured JSON to stdout (for agent/CI consumption)",
|
|
447
582
|
" --debug print file resolution and rule progress to stderr",
|
package/package.json
CHANGED
package/rules/no-string-match.ts
CHANGED
|
@@ -45,7 +45,7 @@ export const noStringMatch: KlintRule = {
|
|
|
45
45
|
})()
|
|
46
46
|
: undefined;
|
|
47
47
|
violations.push({
|
|
48
|
-
file: relative(root, file),
|
|
48
|
+
file: relative(root, file).replaceAll("\\", "/"),
|
|
49
49
|
line: s + 1,
|
|
50
50
|
message: `Use RegExp.exec() instead of String.match() for non-global regexes — use new RegExp(${regexText}).exec(${strText}) instead.`,
|
|
51
51
|
fix,
|