import { readFileSync } from "node:fs"; import { DEFAULT_SERVERS } from "./catalog.js"; import { managedBinDir } from "./platform.js"; // Config-driven design: the plugin resolves its runtime configuration from // pi-lsp.json files in this order (each source may override the previous): // 1. default official-server catalog (src/catalog.ts) // 2. user config — ~/.pi/agent/pi-lsp.json // 3. project config — /.pi/pi-lsp.json (only when the project is trusted) // Unreadable or invalid files are skipped; defaults apply for that source. export interface ConfigPaths { cwd: string; trusted: boolean; userConfigPath?: string; projectConfigPath?: string; /** Environment used for defaults like binDir. Defaults to process.env. */ env?: NodeJS.ProcessEnv; } export interface ServerConfig { /** Override the spawn command (argv array). */ command?: string[]; /** Override which file extensions route to this server. */ extensions?: string[]; /** Explicit languageId; otherwise derived from the file extension. */ languageId?: string; /** Project-root marker files for root detection. */ rootMarkers?: string[]; /** Environment overrides for the server process. */ env?: Record; /** LSP initialization options / workspace configuration values. */ initialization?: Record; /** Opt-in managed install when the binary is missing. */ autoDownload?: boolean; /** Drop this server from the catalog. */ disabled?: boolean; } export type ResolvedServer = Omit & { command: string[]; extensions: string[]; }; export type ProgressInject = "status" | "widget" | "conversation" | "none"; export interface ProgressiveConfig { enabled: boolean; inject: ProgressInject; maxDiagnostics: number; quietMs: number; } export interface LspConfig { timeout: number; /** Directory for managed server installs. */ binDir: string; progressive: ProgressiveConfig; servers: Record; } interface RawConfig { timeout?: number; binDir?: string; progressive?: Partial; servers?: Record; } const DEFAULT_PROGRESSIVE: ProgressiveConfig = { enabled: true, inject: "status", maxDiagnostics: 20, quietMs: 2000, }; const DEFAULT_TIMEOUT = 30_000; /** Load and resolve the effective LSP configuration. Never throws. */ export function loadConfig(paths: ConfigPaths): LspConfig { const user = readConfig(paths.userConfigPath); const project = paths.trusted ? readConfig(paths.projectConfigPath) : undefined; const servers: Record = {}; for (const [id, def] of Object.entries(DEFAULT_SERVERS)) { servers[id] = { command: def.command, extensions: def.extensions }; } applySource(servers, user); applySource(servers, project); const sources = [user, project].filter((x): x is RawConfig => x !== undefined); const last = sources[sources.length - 1]; const timeout = last?.timeout ?? sources[0]?.timeout ?? DEFAULT_TIMEOUT; const binDir = last?.binDir ?? sources[0]?.binDir ?? managedBinDir(paths.env ?? process.env); const progressive: ProgressiveConfig = { ...DEFAULT_PROGRESSIVE, ...(sources[0]?.progressive ?? {}), ...(last?.progressive ?? {}), }; return { timeout, binDir, progressive, servers }; } function applySource( servers: Record, source: RawConfig | undefined, ) { if (!source?.servers) return; for (const [id, cfg] of Object.entries(source.servers)) { if (cfg.disabled) { delete servers[id]; continue; } const existing = servers[id] ?? { command: [], extensions: [] }; servers[id] = { ...existing, ...cfg, } as ResolvedServer; } } function readConfig(file: string | undefined): RawConfig | undefined { if (!file) return undefined; try { const parsed = JSON.parse(readFileSync(file, "utf8")) as RawConfig; if (parsed && typeof parsed === "object") return parsed; return undefined; } catch { return undefined; } }