initial commit
This commit is contained in:
134
src/runtime.ts
Normal file
134
src/runtime.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
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<FormatResult>;
|
||||
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<void>;
|
||||
}
|
||||
|
||||
type LoadedConfig = ReturnType<typeof loadDevToolsConfig>;
|
||||
|
||||
export function createDevToolsRuntime(deps: {
|
||||
cwd: string;
|
||||
agentDir: string;
|
||||
loadConfig?: () => LoadedConfig;
|
||||
knownPaths?: string[];
|
||||
formatterRunner: { formatFile: (input: any) => Promise<FormatResult> };
|
||||
lspBackend: { collectForFile: (input: any) => Promise<DiagnosticsState> };
|
||||
commandBackend: { collect: (input: any) => Promise<DiagnosticsState> };
|
||||
probeProject?: typeof probeProject;
|
||||
}): DevToolsRuntime {
|
||||
const diagnosticsByFile = new Map<string, DiagnosticsState>();
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user