import { readFile } from "node:fs/promises"; import { loadDevToolsConfig } from "./config.ts"; import type { CapabilityGap, DiagnosticsState } from "./diagnostics/types.ts"; import { probeProject } from "./project-probe.ts"; import { resolveProfileForPath } from "./profiles.ts"; import { buildPromptBlock } from "./summary.ts"; export interface FormatResult { status: "formatted" | "skipped" | "failed"; message?: string; } export interface DevToolsRuntime { formatAfterMutation(absolutePath: string): Promise; noteMutation(absolutePath: string, formatResult: FormatResult): void; setDiagnostics(path: string, state: DiagnosticsState): void; recordCapabilityGap(path: string, message: string): void; getPromptBlock(): string | undefined; refreshDiagnosticsForPath(absolutePath: string, text?: string): Promise; } type LoadedConfig = ReturnType; export function createDevToolsRuntime(deps: { cwd: string; agentDir: string; loadConfig?: () => LoadedConfig; knownPaths?: string[]; formatterRunner: { formatFile: (input: any) => Promise }; lspBackend: { collectForFile: (input: any) => Promise }; commandBackend: { collect: (input: any) => Promise }; probeProject?: typeof probeProject; }): DevToolsRuntime { const diagnosticsByFile = new Map(); const capabilityGaps: CapabilityGap[] = []; const getConfig = () => deps.loadConfig?.() ?? loadDevToolsConfig(deps.cwd, deps.agentDir); function setDiagnostics(path: string, state: DiagnosticsState) { diagnosticsByFile.set(path, state); } function recordCapabilityGap(path: string, message: string) { if (!capabilityGaps.some((gap) => gap.path === path && gap.message === message)) { capabilityGaps.push({ path, message }); } } function getPromptBlock() { const config = getConfig(); const maxDiagnosticsPerFile = config?.defaults?.maxDiagnosticsPerFile ?? 10; if (diagnosticsByFile.size === 0 && capabilityGaps.length === 0) return undefined; return buildPromptBlock({ maxDiagnosticsPerFile, diagnosticsByFile, capabilityGaps, }); } async function refreshDiagnosticsForPath(absolutePath: string, text?: string) { const config = getConfig(); if (!config) { recordCapabilityGap(absolutePath, "No dev-tools config found."); return; } const match = resolveProfileForPath(config, absolutePath, deps.cwd, deps.knownPaths ?? []); if (!match) { const probe = await (deps.probeProject ?? probeProject)({ cwd: deps.cwd }); recordCapabilityGap(absolutePath, `No profile matched. ${probe.summary}`); return; } const fileText = text ?? await readFile(absolutePath, "utf8"); for (const backend of match.profile.diagnostics) { if (backend.kind === "lsp") { const lspResult = await deps.lspBackend.collectForFile({ key: `${match.profile.languageId ?? "plain"}:${match.workspaceRoot}`, absolutePath, workspaceRoot: match.workspaceRoot, languageId: match.profile.languageId ?? "plaintext", text: fileText, command: backend.command, }); if (lspResult.status === "ok") { setDiagnostics(absolutePath, lspResult); if (lspResult.message) recordCapabilityGap(absolutePath, lspResult.message); return; } recordCapabilityGap(absolutePath, lspResult.message ?? "LSP diagnostics unavailable."); continue; } const commandResult = await deps.commandBackend.collect({ absolutePath, workspaceRoot: match.workspaceRoot, backend, timeoutMs: config.defaults?.diagnosticTimeoutMs, }); setDiagnostics(absolutePath, commandResult); return; } recordCapabilityGap(absolutePath, "No diagnostics backend succeeded."); } return { async formatAfterMutation(absolutePath: string) { const config = getConfig(); if (!config) return { status: "skipped" as const }; const match = resolveProfileForPath(config, absolutePath, deps.cwd, deps.knownPaths ?? []); if (!match?.profile.formatter) return { status: "skipped" as const }; return deps.formatterRunner.formatFile({ absolutePath, workspaceRoot: match.workspaceRoot, formatter: match.profile.formatter, timeoutMs: config.defaults?.formatTimeoutMs, }); }, noteMutation(absolutePath: string, formatResult: FormatResult) { if (formatResult.status === "failed") { recordCapabilityGap(absolutePath, formatResult.message ?? "Formatter failed."); } void refreshDiagnosticsForPath(absolutePath); }, setDiagnostics, recordCapabilityGap, getPromptBlock, refreshDiagnosticsForPath, }; }