import { promises as fs } from "node:fs";
import path from "node:path";
import { IGNORED_ENTRY_NAMES, resolveToolPath } from "../agent/paths.js";
import type { ToolResult } from "../agent/types.js";
import { loadLspConfig, supportedExtensions } from "./config.js";
import { formatToolDiagnostics } from "./format.js";
import { getLspManager } from "./manager.js";
const MAX_SCAN_FILES = 20;
const MAX_SCAN_DEPTH = 6;
async function collectSourceFiles(
root: string,
extensions: Set<string>,
): Promise<string[]> {
const files: string[] = [];
const visit = async (dir: string, depth: number): Promise<void> => {
if (files.length >= MAX_SCAN_FILES || depth > MAX_SCAN_DEPTH) return;
let entries;
try {
entries = await fs.readdir(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
if (files.length >= MAX_SCAN_FILES) return;
if (entry.name.startsWith(".") || IGNORED_ENTRY_NAMES.has(entry.name)) continue;
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
await visit(full, depth + 1);
continue;
}
if (!entry.isFile()) continue;
const ext = path.extname(entry.name).replace(/^\./, "").toLowerCase();
if (extensions.has(ext)) files.push(full);
}
};
await visit(root, 0);
return files;
}
function clampMaxResults(raw: unknown): number {
if (typeof raw !== "number" || !Number.isFinite(raw)) return 50;
return Math.max(1, Math.min(100, Math.round(raw)));
}
export async function runDiagnosticsTool(input: {
workspace: string;
target: string;
maxResults?: unknown;
allowOutsideWorkspace: boolean;
signal?: AbortSignal;
}): Promise<ToolResult> {
try {
const target = input.target?.trim() || ".";
const resolved = await resolveToolPath(
input.workspace,
target,
input.allowOutsideWorkspace,
);
const stat = await fs.stat(resolved);
const config = await loadLspConfig(input.workspace);
if (config.disabled) {
return {
ok: true,
output: "[diagnostics] LSP diagnostics are disabled by .meowcode/lsp.json.",
};
}
let files: string[];
let scannedDirectory = false;
if (stat.isDirectory()) {
scannedDirectory = true;
files = await collectSourceFiles(resolved, supportedExtensions(config));
if (!files.length) {
return {
ok: true,
output: "[diagnostics] No source files for a configured language server.",
};
}
} else if (stat.isFile()) {
files = [resolved];
} else {
return { ok: false, output: `Not a file or directory: ${target}` };
}
const result = await getLspManager().diagnose({
cwd: input.workspace,
files,
signal: input.signal,
timeoutMs: 6_000,
});
const capped =
scannedDirectory && files.length >= MAX_SCAN_FILES
? "\n[diagnostics] Directory scan capped at 20 files; pass a specific file for full coverage."
: "";
return {
ok: true,
output:
formatToolDiagnostics(
result,
input.workspace,
clampMaxResults(input.maxResults),
) + capped,
};
} catch (error) {
return {
ok: false,
output: error instanceof Error ? error.message : String(error),
};
}
}