import { type SessionWarmup } from "../../core/lsp-session.js"; import { type LspChildProcess } from "./stdio-lsp-transport.js"; import type { LspServerManager, LspSession, LspReadText } from "../../core/lsp.js"; import type { ExecutionEnv } from "../../internal/harness.js"; /** * Default language → server command table (the DETERMINED supported set). Each value is a `command [args…]` * whose binary must be on the host `PATH`; absent ⇒ that language degrades. Mirrors the servers service bakes * into its sandbox templates (so TOC & TOB expose the SAME languages when installed), plus the common * rust/c/cpp servers a local dev box usually has. Override/extend via {@link NodeLspManagerOptions.servers} * (a `null` value disables a language). All of these speak LSP over stdio by default. */ export declare const DEFAULT_LSP_SERVERS: Readonly>; /** Spawn a child (default). Resolves the child on a successful `spawn`, or `undefined` on `error` (ENOENT = * server not installed → the manager degrades that language). Waiting for the 'spawn'/'error' race before use * is CRITICAL: `spawn()` returns synchronously but ENOENT fires 'error' asynchronously (CC's LSPClient §1.5). */ export type LspSpawn = (command: string, args: string[], cwd: string, signal?: AbortSignal) => Promise; export interface NodeLspManagerOptions { /** Override/extend {@link DEFAULT_LSP_SERVERS} (merged over it; a `null` value disables that language). */ servers?: Record; /** Injectable spawn (tests supply a fake server; default = {@link defaultLspSpawn} over `child_process.spawn`). */ spawn?: LspSpawn; /** Read a file's CURRENT text for the session's didOpen/didChange re-sync (default = local `fs.readFile` utf8). */ readText?: LspReadText; /** Resolve the server `cwd`/`rootUri` for a file (default = env.cwd ?? nearest project marker ?? file dir). */ resolveRoot?: (filePath: string, env?: ExecutionEnv) => string; /** Observability hook (open/heal/degrade/evict events). */ log?: (event: string, fields: Record) => void; /** Warm-up window override (see {@link TransportLspSession}); default 10s/1s. */ warmup?: SessionWarmup; /** Max concurrently-cached language servers before LRU eviction (default 16, min 1). A safety net bounding * the cache when a deployment can't call {@link NodeLspManager.evict} at task-end (e.g. ephemeral/worktree * roots). Each cached server is a real process (jdtls = a JVM), so keep this modest. */ maxSessions?: number; /** Idle-TTL (ms) after which an unused server is auto-reclaimed by a background sweep (default 600_000 = 10 * min; 0 disables). Self-healing complement to {@link NodeLspManager.evict}: a deployment that can't call * `evict` at task-end still won't leak — an abandoned server dies on its own. An actively reused server never * goes idle (its last-used stamp refreshes on every lookup), so a modest TTL doesn't churn the happy path. */ idleTtlMs?: number; /** design/121: the workspace diagnostics registry every session feeds from publishDiagnostics. * WORKSPACE-scoped, not server-scoped — it survives heal/evict (diagnostics belong to files, not to a * server instance). Absent ⇒ notifications stay dropped (pre-121 behavior byte-for-byte). */ diagnostics?: import("../../core/lsp-diagnostics.js").LspDiagnosticsRegistry; } export declare class NodeLspManager implements LspServerManager { private readonly servers; private readonly spawnFn; private readonly readTextFn; private readonly resolveRootFn; private readonly log; private readonly warmup?; /** design/121: the workspace diagnostics registry (see {@link NodeLspManagerOptions.diagnostics}) — * public so the Runner drains it at turn boundaries via {@link LspServerManager.diagnostics}. */ readonly diagnostics: import("../../core/lsp-diagnostics.js").LspDiagnosticsRegistry; private readonly maxSessions; private readonly idleTtlMs; private sweepTimer?; /** ` ` → the live session + its root + last-used stamp (reused across ops; healed when its * transport dies; idle-TTL swept; LRU-evicted over {@link maxSessions}; `evict(root)` drops a task's servers * at task-end). Space-separated key is unambiguous — a languageId never contains a space. */ private readonly cache; /** In-flight opens (concurrent ops on the same language+root share ONE spawn/initialize). Tracks each open's * `root` + a `cancelled` flag so {@link evict}/{@link dispose} can reclaim a server whose open resolves AFTER * the teardown already ran (else its `.then` caches a live child that nothing reclaims — permanently after * dispose, since the sweep timer is cleared). */ private readonly opening; /** Set by {@link dispose}: an in-flight open that resolves after dispose self-closes instead of caching. */ private disposed; constructor(opts?: NodeLspManagerOptions); /** Close + drop every server idle past {@link idleTtlMs}. Runs on a timer; best-effort. */ private sweepIdle; sessionFor(filePath: string, signal?: AbortSignal, env?: ExecutionEnv): Promise; /** Spawn + initialize a fresh session for `key`, deduping concurrent opens. */ private open; /** LRU-evict (close) the least-recently-used server(s) until there is room for one more under {@link maxSessions}. * A safety net: without an explicit {@link evict}, a long-lived process spawning tasks with EPHEMERAL roots * (worktree / per-task cwd) would otherwise leak one heavyweight language server per (task × language). */ private evictOverCap; /** * Evict + kill every cached language server rooted at `root`. Call this at TASK-END for an ephemeral/worktree * cwd so a heavyweight server (e.g. a jdtls JVM) doesn't outlive the task that spawned it — the deterministic * complement to the LRU cap. `root` must match what `resolveRoot` produced for the task (the host lane uses the * task's `env.cwd`). Best-effort + idempotent (an unknown/already-evicted root is a no-op). */ evict(root: string): Promise; private openSession; private serverCmd; /** Kill every running server + stop the idle sweep (call on runner/deployment shutdown). Best-effort. */ dispose(): Promise; } /** Resolve a file path to a languageId via its extension (undefined = no mapping → degrade). */ export declare function languageFor(filePath: string): string | undefined; /** service [377] ask ②: on Windows, npm-global language servers materialize as `.cmd` shims and * Node 20+ `spawn(cmd, args)` without a shell rejects them (EINVAL, CVE-2024-27980 hardening) — * every DEFAULT_LSP_SERVERS entry would silently degrade to "no LSP". `shell:true` routes through * cmd.exe so PATHEXT/.cmd resolution works. Injection surface: `command`/`args` come from * LspServerSpec (deployment configuration, trusted) or DEFAULT_LSP_SERVERS constants — never from * the model. Exported as a pure function so the win32 branch is testable off-platform. */ export declare function lspSpawnOptions(cwd: string): { cwd: string; stdio: ["pipe", "pipe", "ignore"]; shell?: boolean; }; /** Default spawn over `child_process.spawn`, resolving on 'spawn' / `undefined` on 'error' (ENOENT = degrade). */ export declare const defaultLspSpawn: LspSpawn; //# sourceMappingURL=node-lsp-manager.d.ts.map