pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
223 lines (221 loc) • 8.44 kB
JavaScript
import { createRequire as __pilensCreateRequire } from "node:module"; const require = __pilensCreateRequire(import.meta.url);
import {
AUTOMATION_FRAMING
} from "../chunk-Y4T27COV.js";
import {
recordTurnEndOutcome,
requestWarmAnalyze,
requestWarmTurnEnd
} from "../chunk-PWFHBTSF.js";
import "../chunk-2EECBNC5.js";
import "../chunk-N3YQJI6O.js";
import {
flushExtensionLog,
logExtension
} from "../chunk-O6TQT6RI.js";
import "../chunk-5THXD6X5.js";
import {
redactSecrets
} from "../chunk-UK3CMEAL.js";
import "../chunk-LJDODBBZ.js";
import "../chunk-Q5U6FMDI.js";
import "../chunk-NXL4FFQQ.js";
// dist/mcp/analyze-cli.js
import * as path from "node:path";
console.log = (...args) => console.error(...args);
function argVal(name) {
const prefix = `--${name}=`;
const found = process.argv.find((value) => value.startsWith(prefix));
return found ? found.slice(prefix.length) : void 0;
}
async function readStdin() {
const chunks = [];
for await (const chunk of process.stdin)
chunks.push(chunk);
return Buffer.concat(chunks).toString("utf8");
}
function formatReport(result, cwd) {
const rel = path.relative(cwd, result.filePath) || result.filePath;
const lines = [
// #2420: advisories (hint/info-tier style opinions) reported under their
// own label — no longer folded into the warning count on this surface.
`\u{1F50E} pi-lens: ${rel} \u2014 ${result.counts.blockers} blocking, ${result.counts.warnings} warning(s), ${result.counts.advisories} advisory(ies)`
];
for (const d of result.diagnostics.slice(0, 30)) {
const marker = d.semantic === "blocking" ? "\u{1F534}" : "\u26A0";
const label = d.rule ?? d.tool;
lines.push(` ${marker} L${d.line ?? "?"} ${label}: ${d.message}`);
}
if (result.diagnostics.length > 30) {
lines.push(` \u2026 ${result.diagnostics.length - 30} more`);
}
if (result.lsp && (result.lsp.status === "skipped" || result.lsp.status === "deferred")) {
lines.push(" (LSP diagnostics were skipped or deferred \u2014 run pilens_analyze on the warm MCP server for type errors)");
}
return lines.join("\n");
}
var STDIN_READ_TIMEOUT_MS = 2e3;
async function readHookPayload() {
if (process.stdin.isTTY)
return void 0;
const raw = await Promise.race([
readStdin(),
new Promise((resolve) => {
const timer = setTimeout(() => resolve(void 0), STDIN_READ_TIMEOUT_MS);
timer.unref();
})
]);
if (raw === void 0) {
process.stdin.pause();
process.stdin.unref?.();
return void 0;
}
try {
return JSON.parse(raw) ?? void 0;
} catch {
return void 0;
}
}
var TURN_END_MAX_LINES = 40;
var TURN_END_MAX_CHARS = 2e3;
var ANSI_AND_CONTROL = new RegExp([
"\\u001B\\][\\s\\S]*?(?:\\u0007|\\u001B\\\\)",
// OSC … BEL/ST
"\\u001B\\[[0-?]*[ -/]*[@-~]",
// CSI — SGR colours et al
"\\u001B[@-Z\\\\-_]",
// two-byte escapes
"[\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F\\u007F]"
// C0 minus newline/tab, plus DEL
].join("|"), "g");
function stripControlSequences(text) {
return text.replace(ANSI_AND_CONTROL, "");
}
function capCodeUnits(text, cap) {
if (text.length <= cap)
return text;
const last = text.charCodeAt(cap - 1);
const splitsPair = last >= 55296 && last <= 56319;
return text.slice(0, splitsPair ? cap - 1 : cap);
}
function formatTurnEnd(response) {
const sections = [response.turnEnd, response.tests].filter((section) => Boolean(section)).map((section) => stripControlSequences(section.split(AUTOMATION_FRAMING).join("")));
if (sections.length === 0)
return void 0;
let out = `\u{1F50E} pi-lens turn-end
${sections.join("\n\n")}`;
const lines = out.split("\n");
if (lines.length > TURN_END_MAX_LINES) {
out = `${lines.slice(0, TURN_END_MAX_LINES).join("\n")}
\u2026 (truncated)`;
}
if (out.length > TURN_END_MAX_CHARS) {
out = `${capCodeUnits(out, TURN_END_MAX_CHARS)}
\u2026 (truncated)`;
}
return out;
}
var TURN_END_SKIP_REASONS = {
"ipc-error": "no warm pi-lens MCP server answered for this workspace, or its build is stale \u2014 start the MCP server, or run `npm run build` if you have edited pi-lens sources",
// #3255: split out of `ipc-error` because its remedy is different and no
// rebuild reaches it. A server that was already running when pi-lens was
// upgraded still owns the endpoint name the previous build derived.
"no-listener": "nothing is listening on this workspace's warm endpoint \u2014 start the MCP server; if pi-lens was upgraded while a server was running, that server still owns the endpoint name of the previous build, so restart it",
timeout: "the warm pi-lens MCP server did not finish the pass within the hook budget \u2014 the turn-end findings are kept and will be delivered on a later Stop",
"schema-mismatch": "the warm pi-lens MCP server speaks a different turn-end schema than this hook binary \u2014 rebuild and restart the server so both halves match",
"stale-answer": "the warm pi-lens MCP server replied about a superseded state \u2014 the findings are kept for a later Stop"
};
async function runTurnEndMode(cwd, payload) {
if (payload?.hook_event_name === "SubagentStop") {
process.stderr.write("pi-lens turn-end skipped: SubagentStop (the main agent's Stop runs the pass)\n");
process.exitCode = 0;
return;
}
const outcome = await requestWarmTurnEnd(cwd);
if (!outcome.available) {
const detail = TURN_END_SKIP_REASONS[outcome.reason];
const message = `\u{1F50E} pi-lens turn-end skipped (${outcome.reason}): ${detail}`;
process.stderr.write(`${message} (cwd: ${cwd})
`);
await writeStdout(message);
recordTurnEndOutcome(cwd, { ran: false, reason: outcome.reason });
process.exitCode = 0;
return;
}
recordTurnEndOutcome(cwd, { ran: true });
const report = formatTurnEnd(outcome.response);
if (report)
await writeStdout(report);
process.exitCode = 0;
}
function writeStdout(text) {
return new Promise((done) => {
process.stdout.write(`${text}
`, () => done());
});
}
function writeReport(report, hookMode) {
return writeStdout(hookMode ? JSON.stringify({
hookSpecificOutput: {
hookEventName: "PostToolUse",
additionalContext: report
}
}) : report);
}
async function main() {
const hookMode = process.argv.includes("--hook");
const withLsp = process.argv.includes("--lsp");
const fileArg = argVal("file");
const turnEndFlag = process.argv.includes("--turn-end");
let cwd = argVal("cwd") ?? process.cwd();
let hookInvocation = hookMode;
let operation = "analyze";
try {
const payload = fileArg === void 0 && !turnEndFlag ? await readHookPayload() : void 0;
cwd = argVal("cwd") ?? payload?.cwd ?? cwd;
const event = payload?.hook_event_name;
const payloadFile = payload?.tool_input?.file_path ?? payload?.tool_input?.path;
hookInvocation ||= Boolean(payloadFile);
if (turnEndFlag || event === "Stop" || event === "SubagentStop") {
hookInvocation = true;
operation = "turn-end";
await runTurnEndMode(cwd, payload);
return;
}
const file = fileArg ?? payloadFile;
if (!file)
process.exit(0);
let result = await requestWarmAnalyze(cwd, file);
if (!result) {
const { analyzeFile } = await import("../chunk-HMFXILPV.js");
result = await analyzeFile(file, cwd, {
flags: withLsp ? {} : { "no-lsp": true },
record: false,
registerTurnState: true
});
}
const { flushProbeCache } = await import("../probes/installer.js");
await flushProbeCache();
if (result.counts.diagnostics === 0)
process.exit(0);
await writeReport(formatReport(result, cwd), hookMode);
process.exit(0);
} catch (err) {
const detail = redactSecrets(stripControlSequences(err instanceof Error ? err.message : String(err))).replace(/\s+/g, " ");
const reason = detail.length > 1e3 ? `${capCodeUnits(detail, 1e3)}\u2026 (truncated)` : detail;
const message = `pi-lens-analyze failed: ${reason}`;
recordTurnEndOutcome(cwd, { failed: true, operation, reason });
logExtension({
subsystem: "analyze-cli",
message: "analyze-cli-failed",
metadata: { cwd, operation, reason }
});
process.stderr.write(`${message}
`);
await writeReport(message, hookMode && operation === "analyze");
await flushExtensionLog();
process.exit(hookInvocation ? 0 : 2);
}
}
void main();