pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
106 lines (105 loc) • 3.76 kB
JavaScript
import * as fs from "node:fs";
import * as path from "node:path";
import { safeSpawnAsync } from "../../safe-spawn.js";
import { PRIORITY } from "../priorities.js";
import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
const vale = createAvailabilityChecker("vale", ".exe");
/**
* Check for a .vale.ini config file in cwd or parent dirs.
*/
function findValeConfig(cwd) {
const local = path.join(cwd, ".vale.ini");
if (fs.existsSync(local))
return local;
let current = path.resolve(cwd);
while (true) {
const candidate = path.join(current, ".vale.ini");
if (fs.existsSync(candidate))
return candidate;
const parent = path.dirname(current);
if (parent === current)
break;
current = parent;
}
return undefined;
}
function parseValeOutput(raw, filePath) {
const diagnostics = [];
if (!raw.trim())
return diagnostics;
try {
const parsed = JSON.parse(raw);
const files = parsed?.Data?.Files;
if (!files)
return diagnostics;
for (const file of files) {
if (!file.Alerts)
continue;
for (const alert of file.Alerts) {
if (!alert.Message)
continue;
const severityMap = {
error: "error",
warning: "warning",
info: "info",
suggestion: "info",
};
const severity = severityMap[alert.Severity?.toLowerCase() ?? ""] ?? "warning";
diagnostics.push({
id: `vale-${alert.Line}-${alert.Check ?? "unknown"}`,
message: `[${alert.Check ?? "vale"}] ${alert.Message}`,
filePath,
line: alert.Line ?? 1,
column: alert.Column ?? 1,
severity,
semantic: severity === "error" ? "blocking" : "warning",
tool: "vale",
rule: alert.Check ?? "vale",
fixable: false,
});
}
}
}
catch {
// JSON parse failed, return empty
return diagnostics;
}
return diagnostics;
}
const valeRunner = {
id: "vale",
appliesTo: ["markdown"],
priority: PRIORITY.DOC_QUALITY,
enabledByDefault: true,
skipTestFiles: false,
async run(ctx) {
const cwd = ctx.cwd || process.cwd();
// Config-gated: skip unless a .vale.ini is found
if (!findValeConfig(cwd)) {
return { status: "skipped", diagnostics: [], semantic: "none" };
}
let cmd = null;
if (await (vale.isAvailableAsync(cwd))) {
cmd = vale.getCommand(cwd);
}
else {
cmd = await resolveToolCommandWithInstallFallback(cwd, "vale");
}
if (!cmd)
return { status: "skipped", diagnostics: [], semantic: "none" };
// Vale exits 0 even on findings, non-zero only on errors
const result = await safeSpawnAsync(cmd, ["--output", "JSON", ctx.filePath], { cwd, timeout: 15000 });
const raw = result.stdout || "";
const diagnostics = parseValeOutput(raw, ctx.filePath);
if (diagnostics.length === 0) {
return { status: "succeeded", diagnostics: [], semantic: "none" };
}
const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
return {
status: hasBlocking ? "failed" : "succeeded",
diagnostics,
semantic: hasBlocking ? "blocking" : "warning",
};
},
};
export default valeRunner;