opencode-agent-skill 15.0.0 → 15.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/docs/V15-MANAGED-RUNTIME.md +18 -0
- package/global-config/agents/verifier.md +6 -2
- package/lib/affected-tests.mjs +58 -8
- package/lib/capability-fabric.mjs +107 -48
- package/lib/code-intelligence/index.mjs +4 -15
- package/lib/code-intelligence/lsp-pool.mjs +694 -0
- package/lib/code-intelligence/lsp-provider.mjs +481 -67
- package/lib/compaction-resume-guard.mjs +327 -0
- package/lib/completion-auditor.mjs +20 -0
- package/lib/context-engine-v11.mjs +20 -7
- package/lib/context-manifest.mjs +38 -21
- package/lib/decision-policy.mjs +40 -2
- package/lib/evidence-store.mjs +121 -31
- package/lib/executable-probe.mjs +69 -0
- package/lib/execution-contract.mjs +29 -16
- package/lib/hierarchical-context.mjs +31 -3
- package/lib/learning-engine.mjs +164 -65
- package/lib/memory-engine.mjs +348 -98
- package/lib/model-config.mjs +86 -7
- package/lib/performance-fabric.mjs +354 -107
- package/lib/permission-policy.mjs +232 -0
- package/lib/pi-rpc-pool.mjs +170 -5
- package/lib/provider-recovery.mjs +51 -0
- package/lib/repo-graph.mjs +38 -5
- package/lib/runtime-events.mjs +31 -10
- package/lib/semantic-index.mjs +67 -20
- package/lib/skill-compiler.mjs +84 -18
- package/lib/subagent-artifacts.mjs +182 -0
- package/lib/task-policy.mjs +80 -7
- package/lib/trajectory.mjs +25 -1
- package/lib/untrusted-output.mjs +106 -0
- package/lib/verification-broker.mjs +32 -27
- package/lib/workspace-fingerprint.mjs +43 -8
- package/lib/workspace-hygiene.mjs +481 -0
- package/package.json +8 -6
- package/pi/extensions/ues-child-runtime.ts +103 -5
- package/pi/extensions/ues.ts +984 -95
- package/scripts/benchmark-lsp-pool.mjs +128 -0
- package/scripts/check-release-consistency.mjs +16 -4
- package/scripts/check-source-integrity.mjs +60 -1
- package/scripts/run-test-suite.mjs +143 -0
- package/scripts/smoke-package-closure.mjs +4 -0
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { spawn
|
|
1
|
+
import { spawn } from "node:child_process"
|
|
2
|
+
import { existsSync } from "node:fs"
|
|
2
3
|
import { lstat, readFile, realpath } from "node:fs/promises"
|
|
3
4
|
import path from "node:path"
|
|
4
|
-
import {
|
|
5
|
+
import { commandExists } from "../executable-probe.mjs"
|
|
6
|
+
import { resolveWindowsCommand } from "../windows-shim.mjs"
|
|
7
|
+
import { lspPoolStatus as managedLspPoolStatus, shutdownLspPool as shutdownManagedLspPool, withManagedLspSession } from "./lsp-pool.mjs"
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
5
9
|
|
|
6
10
|
const PROVIDERS = [
|
|
7
11
|
{ id: "typescript-language-server", extensions: new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]), commands: [["typescript-language-server", ["--stdio"]]], languageId: (ext) => [".ts", ".tsx"].includes(ext) ? "typescript" : "javascript" },
|
|
@@ -11,18 +15,16 @@ const PROVIDERS = [
|
|
|
11
15
|
{ id: "clangd", extensions: new Set([".c", ".cc", ".cpp", ".cxx", ".h", ".hpp"]), commands: [["clangd", []]], languageId: (ext) => [".c", ".h"].includes(ext) ? "c" : "cpp" },
|
|
12
16
|
]
|
|
13
17
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return available
|
|
25
|
-
}
|
|
18
|
+
export const LSP_OPERATIONS = Object.freeze([
|
|
19
|
+
"diagnostics",
|
|
20
|
+
"definition",
|
|
21
|
+
"references",
|
|
22
|
+
"symbols",
|
|
23
|
+
"hover",
|
|
24
|
+
"rename-preview",
|
|
25
|
+
"incoming-calls",
|
|
26
|
+
"outgoing-calls",
|
|
27
|
+
])
|
|
26
28
|
|
|
27
29
|
function inside(root, target) {
|
|
28
30
|
return target === root || target.startsWith(root + path.sep)
|
|
@@ -31,26 +33,81 @@ function inside(root, target) {
|
|
|
31
33
|
async function safeFile(root, relative) {
|
|
32
34
|
const base = await realpath(path.resolve(root)).catch(() => path.resolve(root))
|
|
33
35
|
const requested = path.resolve(base, String(relative || ""))
|
|
34
|
-
if (!inside(base, requested)) throw new Error("
|
|
36
|
+
if (!inside(base, requested)) throw new Error("LSP path escapes workspace root")
|
|
35
37
|
const info = await lstat(requested)
|
|
36
|
-
if (!info.isFile()) throw new Error("
|
|
38
|
+
if (!info.isFile()) throw new Error("LSP path is not a file")
|
|
37
39
|
const actual = await realpath(requested)
|
|
38
|
-
if (!inside(base, actual)) throw new Error("
|
|
40
|
+
if (!inside(base, actual)) throw new Error("LSP symlink escapes workspace root")
|
|
39
41
|
return { base, file: actual, info }
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
export function resolveTypeScriptTsserverFallback(options = {}) {
|
|
45
|
+
const explicit = String(options.explicitPath ?? process.env.UES_TYPESCRIPT_TSSERVER_PATH ?? "").trim()
|
|
46
|
+
if (explicit && existsSync(path.resolve(explicit))) return path.resolve(explicit)
|
|
47
|
+
|
|
48
|
+
const platform = String(options.platform || process.platform)
|
|
49
|
+
if (platform !== "win32") return null
|
|
50
|
+
|
|
51
|
+
const execution = options.execution || resolveWindowsCommand("tsserver")
|
|
52
|
+
const candidates = [
|
|
53
|
+
execution?.entry,
|
|
54
|
+
...(Array.isArray(execution?.argsPrefix) ? execution.argsPrefix : []),
|
|
55
|
+
].filter(Boolean)
|
|
56
|
+
|
|
57
|
+
for (const value of candidates) {
|
|
58
|
+
const entry = path.resolve(String(value))
|
|
59
|
+
const binDir = path.dirname(entry)
|
|
60
|
+
const packageRoot = path.basename(binDir).toLowerCase() === "bin"
|
|
61
|
+
? path.dirname(binDir)
|
|
62
|
+
: null
|
|
63
|
+
const preferred = packageRoot ? path.join(packageRoot, "lib", "tsserver.js") : null
|
|
64
|
+
if (preferred && existsSync(preferred)) return preferred
|
|
65
|
+
if (existsSync(entry)) return entry
|
|
66
|
+
}
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function providerRuntimeOptions(provider) {
|
|
71
|
+
if (provider?.id !== "typescript-language-server") return {}
|
|
72
|
+
const fallbackPath = resolveTypeScriptTsserverFallback()
|
|
73
|
+
return fallbackPath
|
|
74
|
+
? { initializationOptions: { tsserver: { fallbackPath } } }
|
|
75
|
+
: {}
|
|
76
|
+
}
|
|
77
|
+
|
|
42
78
|
function providerFor(file) {
|
|
43
79
|
const ext = path.extname(file).toLowerCase()
|
|
44
80
|
for (const provider of PROVIDERS) {
|
|
45
81
|
if (!provider.extensions.has(ext)) continue
|
|
46
82
|
for (const [command, args] of provider.commands) {
|
|
47
|
-
if (commandExists(command))
|
|
83
|
+
if (commandExists(command)) {
|
|
84
|
+
return {
|
|
85
|
+
...provider,
|
|
86
|
+
...providerRuntimeOptions(provider),
|
|
87
|
+
command,
|
|
88
|
+
args,
|
|
89
|
+
languageId: provider.languageId(ext),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
48
92
|
}
|
|
49
93
|
return { ...provider, command: null, args: [], languageId: provider.languageId(ext) }
|
|
50
94
|
}
|
|
51
95
|
return null
|
|
52
96
|
}
|
|
53
97
|
|
|
98
|
+
export function lspPersistencePolicy(options = {}) {
|
|
99
|
+
if (typeof options.persistent === "boolean") {
|
|
100
|
+
return { enabled: options.persistent, source: "explicit-option" }
|
|
101
|
+
}
|
|
102
|
+
const env = String(process.env.UES_LSP_PERSISTENT || "").trim()
|
|
103
|
+
if (env === "0") return { enabled: false, source: "env-disabled" }
|
|
104
|
+
if (env === "1") return { enabled: true, source: "env-enabled" }
|
|
105
|
+
if (process.env.UES_CHILD_PROCESS === "1") {
|
|
106
|
+
return { enabled: true, source: "pi-child-runtime" }
|
|
107
|
+
}
|
|
108
|
+
return { enabled: false, source: "short-lived-default" }
|
|
109
|
+
}
|
|
110
|
+
|
|
54
111
|
export function lspProviderStatus(file = "") {
|
|
55
112
|
const ext = path.extname(String(file || "")).toLowerCase()
|
|
56
113
|
const providers = PROVIDERS
|
|
@@ -59,7 +116,20 @@ export function lspProviderStatus(file = "") {
|
|
|
59
116
|
const selected = provider.commands.find(([command]) => commandExists(command)) || null
|
|
60
117
|
return { id: provider.id, available: Boolean(selected), command: selected?.[0] || null, extensions: [...provider.extensions] }
|
|
61
118
|
})
|
|
62
|
-
return {
|
|
119
|
+
return {
|
|
120
|
+
schemaVersion: 2,
|
|
121
|
+
extension: ext,
|
|
122
|
+
available: providers.some((row) => row.available),
|
|
123
|
+
operations: [...LSP_OPERATIONS],
|
|
124
|
+
persistentPool: {
|
|
125
|
+
...managedLspPoolStatus({
|
|
126
|
+
includeSessions: false,
|
|
127
|
+
persistent: lspPersistencePolicy().enabled,
|
|
128
|
+
}),
|
|
129
|
+
policy: lspPersistencePolicy(),
|
|
130
|
+
},
|
|
131
|
+
providers,
|
|
132
|
+
}
|
|
63
133
|
}
|
|
64
134
|
|
|
65
135
|
function encodeMessage(payload) {
|
|
@@ -67,6 +137,23 @@ function encodeMessage(payload) {
|
|
|
67
137
|
return Buffer.concat([Buffer.from("Content-Length: " + body.length + "\r\n\r\n", "ascii"), body])
|
|
68
138
|
}
|
|
69
139
|
|
|
140
|
+
function relativeUri(base, uri) {
|
|
141
|
+
try {
|
|
142
|
+
if (!String(uri || "").startsWith("file:")) return String(uri || "")
|
|
143
|
+
const file = fileURLToPath(uri)
|
|
144
|
+
if (!inside(base, file)) return String(uri || "")
|
|
145
|
+
return path.relative(base, file).replaceAll("\\", "/")
|
|
146
|
+
} catch {
|
|
147
|
+
return String(uri || "")
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function position(options = {}) {
|
|
152
|
+
const line = Math.max(1, Math.trunc(Number(options.line || 1)))
|
|
153
|
+
const character = Math.max(1, Math.trunc(Number(options.character || 1)))
|
|
154
|
+
return { line: line - 1, character: character - 1 }
|
|
155
|
+
}
|
|
156
|
+
|
|
70
157
|
function sanitizeDiagnostic(value = {}) {
|
|
71
158
|
return {
|
|
72
159
|
range: value.range || null,
|
|
@@ -77,73 +164,232 @@ function sanitizeDiagnostic(value = {}) {
|
|
|
77
164
|
}
|
|
78
165
|
}
|
|
79
166
|
|
|
80
|
-
|
|
167
|
+
function sanitizeLocations(base, value, limit = 120) {
|
|
168
|
+
const list = Array.isArray(value) ? value : value ? [value] : []
|
|
169
|
+
return list.slice(0, limit).map((item) => {
|
|
170
|
+
if (item?.targetUri) {
|
|
171
|
+
return {
|
|
172
|
+
file: relativeUri(base, item.targetUri),
|
|
173
|
+
range: item.targetRange || null,
|
|
174
|
+
selectionRange: item.targetSelectionRange || null,
|
|
175
|
+
originSelectionRange: item.originSelectionRange || null,
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
file: relativeUri(base, item?.uri),
|
|
180
|
+
range: item?.range || null,
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function flattenSymbols(base, values, output, parent = null, limit = 160) {
|
|
186
|
+
for (const item of Array.isArray(values) ? values : []) {
|
|
187
|
+
if (output.length >= limit) break
|
|
188
|
+
output.push({
|
|
189
|
+
name: String(item?.name || "").slice(0, 300),
|
|
190
|
+
detail: item?.detail == null ? null : String(item.detail).slice(0, 500),
|
|
191
|
+
kind: item?.kind ?? null,
|
|
192
|
+
containerName: item?.containerName || parent,
|
|
193
|
+
file: item?.location?.uri ? relativeUri(base, item.location.uri) : null,
|
|
194
|
+
range: item?.range || item?.location?.range || null,
|
|
195
|
+
selectionRange: item?.selectionRange || null,
|
|
196
|
+
})
|
|
197
|
+
if (Array.isArray(item?.children)) flattenSymbols(base, item.children, output, item?.name || parent, limit)
|
|
198
|
+
}
|
|
199
|
+
return output
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function hoverText(contents) {
|
|
203
|
+
if (contents == null) return ""
|
|
204
|
+
if (typeof contents === "string") return contents
|
|
205
|
+
if (Array.isArray(contents)) {
|
|
206
|
+
return contents.map((item) => typeof item === "string" ? item : item?.value || "").filter(Boolean).join("\n")
|
|
207
|
+
}
|
|
208
|
+
if (typeof contents === "object") return String(contents.value || "")
|
|
209
|
+
return String(contents)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function sanitizeWorkspaceEdit(base, edit, limit = 240) {
|
|
213
|
+
const rows = []
|
|
214
|
+
for (const [uri, edits] of Object.entries(edit?.changes || {})) {
|
|
215
|
+
for (const item of Array.isArray(edits) ? edits : []) {
|
|
216
|
+
rows.push({ file: relativeUri(base, uri), range: item.range || null, newText: String(item.newText || "").slice(0, 4000) })
|
|
217
|
+
if (rows.length >= limit) return { edits: rows, truncated: true }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
for (const change of Array.isArray(edit?.documentChanges) ? edit.documentChanges : []) {
|
|
221
|
+
const uri = change?.textDocument?.uri
|
|
222
|
+
for (const item of Array.isArray(change?.edits) ? change.edits : []) {
|
|
223
|
+
rows.push({ file: relativeUri(base, uri), range: item.range || null, newText: String(item.newText || "").slice(0, 4000) })
|
|
224
|
+
if (rows.length >= limit) return { edits: rows, truncated: true }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return { edits: rows, truncated: false }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function sanitizeHierarchyItem(base, item) {
|
|
231
|
+
if (!item) return null
|
|
232
|
+
return {
|
|
233
|
+
name: String(item.name || "").slice(0, 300),
|
|
234
|
+
detail: item.detail == null ? null : String(item.detail).slice(0, 500),
|
|
235
|
+
kind: item.kind ?? null,
|
|
236
|
+
file: relativeUri(base, item.uri),
|
|
237
|
+
range: item.range || null,
|
|
238
|
+
selectionRange: item.selectionRange || null,
|
|
239
|
+
_raw: item,
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function sanitizeHierarchyCalls(base, values, direction, limit = 120) {
|
|
244
|
+
return (Array.isArray(values) ? values : []).slice(0, limit).map((call) => {
|
|
245
|
+
const item = direction === "incoming" ? call.from : call.to
|
|
246
|
+
const clean = sanitizeHierarchyItem(base, item)
|
|
247
|
+
if (clean) delete clean._raw
|
|
248
|
+
return {
|
|
249
|
+
item: clean,
|
|
250
|
+
ranges: direction === "incoming" ? (call.fromRanges || []) : (call.fromRanges || []),
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function withEphemeralLspSession(root, relative, options, operation) {
|
|
81
256
|
const target = await safeFile(root, relative)
|
|
82
257
|
const provider = providerFor(target.file)
|
|
83
258
|
const relativeFile = path.relative(target.base, target.file).replaceAll("\\", "/")
|
|
84
259
|
if (!provider?.command) {
|
|
85
|
-
return {
|
|
260
|
+
return {
|
|
261
|
+
schemaVersion: 2,
|
|
262
|
+
file: relativeFile,
|
|
263
|
+
available: false,
|
|
264
|
+
provider: provider?.id || null,
|
|
265
|
+
operation: options.operation || null,
|
|
266
|
+
reason: provider ? "lsp-command-unavailable" : "unsupported-extension",
|
|
267
|
+
result: null,
|
|
268
|
+
}
|
|
86
269
|
}
|
|
87
270
|
|
|
88
271
|
const maxBytes = Math.max(8 * 1024, Math.min(4 * 1024 * 1024, Number(options.maxBytes || 1024 * 1024)))
|
|
89
|
-
if (target.info.size > maxBytes) throw new Error("
|
|
272
|
+
if (target.info.size > maxBytes) throw new Error("LSP file exceeds limit (" + target.info.size + " > " + maxBytes + " bytes)")
|
|
90
273
|
const source = await readFile(target.file, "utf8")
|
|
91
274
|
const uri = pathToFileURL(target.file).href
|
|
92
275
|
const rootUri = pathToFileURL(target.base + path.sep).href
|
|
93
|
-
const timeoutMs = Math.max(500, Math.min(
|
|
94
|
-
|
|
276
|
+
const timeoutMs = Math.max(500, Math.min(15_000, Number(options.timeoutMs || 5000)))
|
|
277
|
+
|
|
278
|
+
const execution = process.platform === "win32"
|
|
279
|
+
? resolveWindowsCommand(provider.command)
|
|
280
|
+
: { executable: provider.command, argsPrefix: [] }
|
|
281
|
+
if (!execution?.executable) {
|
|
282
|
+
return {
|
|
283
|
+
schemaVersion: 2,
|
|
284
|
+
file: relativeFile,
|
|
285
|
+
available: true,
|
|
286
|
+
provider: provider.id,
|
|
287
|
+
command: provider.command,
|
|
288
|
+
operation: options.operation || null,
|
|
289
|
+
reason: "spawn-error",
|
|
290
|
+
result: null,
|
|
291
|
+
stderr: null,
|
|
292
|
+
error: "unable to resolve LSP executable: " + provider.command,
|
|
293
|
+
}
|
|
294
|
+
}
|
|
95
295
|
|
|
96
296
|
return await new Promise((resolve) => {
|
|
97
|
-
const child = spawn(
|
|
297
|
+
const child = spawn(execution.executable, [...(execution.argsPrefix || []), ...provider.args], {
|
|
298
|
+
cwd: target.base,
|
|
299
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
300
|
+
windowsHide: true,
|
|
301
|
+
shell: false,
|
|
302
|
+
})
|
|
98
303
|
let buffer = Buffer.alloc(0)
|
|
99
304
|
let stderr = ""
|
|
100
305
|
let settled = false
|
|
101
|
-
let
|
|
102
|
-
|
|
306
|
+
let nextId = 1
|
|
307
|
+
const pending = new Map()
|
|
308
|
+
const notificationWaiters = new Map()
|
|
309
|
+
const recentNotifications = new Map()
|
|
103
310
|
let timer = null
|
|
104
311
|
|
|
105
|
-
const finish = (reason, error = null) => {
|
|
312
|
+
const finish = (reason, result = null, error = null) => {
|
|
106
313
|
if (settled) return
|
|
107
314
|
settled = true
|
|
108
315
|
if (timer) clearTimeout(timer)
|
|
316
|
+
for (const waiter of pending.values()) waiter.reject(new Error("LSP session settled: " + reason))
|
|
317
|
+
pending.clear()
|
|
109
318
|
try {
|
|
110
|
-
if (
|
|
111
|
-
child.stdin.write(encodeMessage({ jsonrpc: "2.0", id: 2, method: "shutdown", params: null }))
|
|
112
|
-
child.stdin.write(encodeMessage({ jsonrpc: "2.0", method: "exit", params: null }))
|
|
113
|
-
}
|
|
319
|
+
if (child.stdin.writable) child.stdin.write(encodeMessage({ jsonrpc: "2.0", method: "exit", params: null }))
|
|
114
320
|
} catch {}
|
|
115
321
|
setTimeout(() => { try { child.kill() } catch {} }, 50).unref?.()
|
|
116
322
|
resolve({
|
|
117
|
-
schemaVersion:
|
|
323
|
+
schemaVersion: 2,
|
|
118
324
|
file: relativeFile,
|
|
119
325
|
available: true,
|
|
120
326
|
provider: provider.id,
|
|
121
327
|
command: provider.command,
|
|
122
|
-
|
|
328
|
+
operation: options.operation || null,
|
|
123
329
|
reason,
|
|
330
|
+
result,
|
|
124
331
|
stderr: stderr.trim().slice(0, 2000) || null,
|
|
125
|
-
error: error ? String(error instanceof Error ? error.message : error).slice(0,
|
|
332
|
+
error: error ? String(error instanceof Error ? error.message : error).slice(0, 1200) : null,
|
|
126
333
|
})
|
|
127
334
|
}
|
|
128
335
|
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
336
|
+
const send = (payload) => {
|
|
337
|
+
if (settled || !child.stdin.writable) throw new Error("LSP stdin unavailable")
|
|
338
|
+
child.stdin.write(encodeMessage(payload))
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const request = (method, params) => new Promise((res, rej) => {
|
|
342
|
+
const id = nextId++
|
|
343
|
+
pending.set(id, { resolve: res, reject: rej, method })
|
|
344
|
+
try { send({ jsonrpc: "2.0", id, method, params }) }
|
|
345
|
+
catch (error) { pending.delete(id); rej(error) }
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
const notify = (method, params) => send({ jsonrpc: "2.0", method, params })
|
|
349
|
+
|
|
350
|
+
const waitForNotification = (method) => {
|
|
351
|
+
if (recentNotifications.has(method)) {
|
|
352
|
+
const value = recentNotifications.get(method)
|
|
353
|
+
recentNotifications.delete(method)
|
|
354
|
+
return Promise.resolve(value)
|
|
355
|
+
}
|
|
356
|
+
return new Promise((res) => {
|
|
357
|
+
const list = notificationWaiters.get(method) || []
|
|
358
|
+
list.push(res)
|
|
359
|
+
notificationWaiters.set(method, list)
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const dispatch = (message) => {
|
|
364
|
+
if (message?.id != null && pending.has(message.id)) {
|
|
365
|
+
const waiter = pending.get(message.id)
|
|
366
|
+
pending.delete(message.id)
|
|
367
|
+
if (message.error) waiter.reject(new Error(String(message.error.message || "LSP request failed")))
|
|
368
|
+
else waiter.resolve(message.result)
|
|
142
369
|
return
|
|
143
370
|
}
|
|
144
|
-
if (message?.method
|
|
145
|
-
|
|
146
|
-
|
|
371
|
+
if (message?.method) {
|
|
372
|
+
// Language servers may issue configuration/capability requests. Reply
|
|
373
|
+
// deterministically so the server does not stall waiting on a client UI.
|
|
374
|
+
if (message.id != null) {
|
|
375
|
+
try {
|
|
376
|
+
const result = message.method === "workspace/configuration"
|
|
377
|
+
? (Array.isArray(message.params?.items) ? message.params.items.map(() => null) : [])
|
|
378
|
+
: null
|
|
379
|
+
send({ jsonrpc: "2.0", id: message.id, result })
|
|
380
|
+
} catch {}
|
|
381
|
+
}
|
|
382
|
+
const waiters = notificationWaiters.get(message.method) || []
|
|
383
|
+
if (waiters.length) {
|
|
384
|
+
notificationWaiters.delete(message.method)
|
|
385
|
+
for (const waiter of waiters) waiter(message.params)
|
|
386
|
+
} else {
|
|
387
|
+
recentNotifications.set(message.method, message.params)
|
|
388
|
+
if (recentNotifications.size > 16) {
|
|
389
|
+
const oldest = recentNotifications.keys().next().value
|
|
390
|
+
if (oldest) recentNotifications.delete(oldest)
|
|
391
|
+
}
|
|
392
|
+
}
|
|
147
393
|
}
|
|
148
394
|
}
|
|
149
395
|
|
|
@@ -159,31 +405,199 @@ export async function diagnoseCode(root, relative, options = {}) {
|
|
|
159
405
|
if (buffer.length < bodyStart + length) return
|
|
160
406
|
const body = buffer.subarray(bodyStart, bodyStart + length).toString("utf8")
|
|
161
407
|
buffer = buffer.subarray(bodyStart + length)
|
|
162
|
-
try {
|
|
408
|
+
try { dispatch(JSON.parse(body)) } catch {}
|
|
163
409
|
}
|
|
164
410
|
}
|
|
165
411
|
|
|
166
412
|
child.stdout.on("data", (chunk) => { buffer = Buffer.concat([buffer, Buffer.from(chunk)]); parse() })
|
|
167
413
|
child.stderr.on("data", (chunk) => { if (stderr.length < 64 * 1024) stderr += chunk.toString() })
|
|
168
|
-
child.on("error", (error) => finish("spawn-error", error))
|
|
414
|
+
child.on("error", (error) => finish("spawn-error", null, error))
|
|
169
415
|
child.on("close", (code) => { if (!settled) finish("server-exit:" + (code ?? "unknown")) })
|
|
170
416
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
417
|
+
timer = setTimeout(() => finish("timeout"), timeoutMs)
|
|
418
|
+
timer.unref?.()
|
|
419
|
+
|
|
420
|
+
;(async () => {
|
|
421
|
+
try {
|
|
422
|
+
await request("initialize", {
|
|
177
423
|
processId: process.pid,
|
|
178
424
|
rootUri,
|
|
179
|
-
capabilities: {
|
|
425
|
+
capabilities: {
|
|
426
|
+
textDocument: {
|
|
427
|
+
publishDiagnostics: { relatedInformation: true },
|
|
428
|
+
definition: { linkSupport: true },
|
|
429
|
+
references: {},
|
|
430
|
+
documentSymbol: { hierarchicalDocumentSymbolSupport: true },
|
|
431
|
+
hover: { contentFormat: ["markdown", "plaintext"] },
|
|
432
|
+
rename: { prepareSupport: true },
|
|
433
|
+
callHierarchy: {},
|
|
434
|
+
},
|
|
435
|
+
workspace: { workspaceFolders: true, applyEdit: false },
|
|
436
|
+
},
|
|
180
437
|
workspaceFolders: [{ uri: rootUri, name: path.basename(target.base) || "workspace" }],
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
438
|
+
initializationOptions: provider.initializationOptions || undefined,
|
|
439
|
+
})
|
|
440
|
+
notify("initialized", {})
|
|
441
|
+
notify("textDocument/didOpen", {
|
|
442
|
+
textDocument: { uri, languageId: provider.languageId, version: 1, text: source },
|
|
443
|
+
})
|
|
444
|
+
const result = await operation({
|
|
445
|
+
request,
|
|
446
|
+
notify,
|
|
447
|
+
waitForNotification,
|
|
448
|
+
uri,
|
|
449
|
+
base: target.base,
|
|
450
|
+
file: relativeFile,
|
|
451
|
+
})
|
|
452
|
+
finish("ok", result)
|
|
453
|
+
} catch (error) {
|
|
454
|
+
finish("request-error", null, error)
|
|
455
|
+
}
|
|
456
|
+
})()
|
|
188
457
|
})
|
|
189
458
|
}
|
|
459
|
+
|
|
460
|
+
export async function lspOperation(root, relative, operation, options = {}) {
|
|
461
|
+
const op = String(operation || "")
|
|
462
|
+
if (!LSP_OPERATIONS.includes(op)) throw new Error("unsupported LSP operation: " + op)
|
|
463
|
+
|
|
464
|
+
const target = await safeFile(root, relative)
|
|
465
|
+
const provider = providerFor(target.file)
|
|
466
|
+
const relativeFile = path.relative(target.base, target.file).replaceAll("\\", "/")
|
|
467
|
+
if (!provider?.command) {
|
|
468
|
+
return {
|
|
469
|
+
schemaVersion: 2,
|
|
470
|
+
file: relativeFile,
|
|
471
|
+
available: false,
|
|
472
|
+
provider: provider?.id || null,
|
|
473
|
+
operation: op,
|
|
474
|
+
reason: provider ? "lsp-command-unavailable" : "unsupported-extension",
|
|
475
|
+
result: null,
|
|
476
|
+
persistent: false,
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const maxResults = Math.max(1, Math.min(500, Number(options.maxResults || 120)))
|
|
481
|
+
const execute = async (session) => {
|
|
482
|
+
const pos = position(options)
|
|
483
|
+
const textDocument = { uri: session.uri }
|
|
484
|
+
|
|
485
|
+
if (op === "diagnostics") {
|
|
486
|
+
const params = await session.waitForNotification("textDocument/publishDiagnostics", {
|
|
487
|
+
predicate: (value) =>
|
|
488
|
+
(!value?.uri || value.uri === session.uri) &&
|
|
489
|
+
(value?.version == null || Number(value.version) >= Number(session.documentSync?.version || 0)),
|
|
490
|
+
afterAt: session.documentSync?.changed ? session.documentSync.syncedAt : 0,
|
|
491
|
+
})
|
|
492
|
+
const values = Array.isArray(params?.diagnostics) ? params.diagnostics : []
|
|
493
|
+
return { diagnostics: values.slice(0, maxResults).map(sanitizeDiagnostic) }
|
|
494
|
+
}
|
|
495
|
+
if (op === "definition") {
|
|
496
|
+
const result = await session.request("textDocument/definition", { textDocument, position: pos })
|
|
497
|
+
return { locations: sanitizeLocations(session.base, result, maxResults) }
|
|
498
|
+
}
|
|
499
|
+
if (op === "references") {
|
|
500
|
+
const result = await session.request("textDocument/references", {
|
|
501
|
+
textDocument,
|
|
502
|
+
position: pos,
|
|
503
|
+
context: { includeDeclaration: options.includeDeclaration !== false },
|
|
504
|
+
})
|
|
505
|
+
return { locations: sanitizeLocations(session.base, result, maxResults) }
|
|
506
|
+
}
|
|
507
|
+
if (op === "symbols") {
|
|
508
|
+
const result = await session.request("textDocument/documentSymbol", { textDocument })
|
|
509
|
+
return { symbols: flattenSymbols(session.base, result, [], null, maxResults) }
|
|
510
|
+
}
|
|
511
|
+
if (op === "hover") {
|
|
512
|
+
const result = await session.request("textDocument/hover", { textDocument, position: pos })
|
|
513
|
+
return {
|
|
514
|
+
range: result?.range || null,
|
|
515
|
+
markdown: hoverText(result?.contents).slice(0, Number(options.maxHoverChars || 12000)),
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (op === "rename-preview") {
|
|
519
|
+
const newName = String(options.newName || "").trim()
|
|
520
|
+
if (!newName) throw new Error("rename-preview requires newName")
|
|
521
|
+
let prepared = null
|
|
522
|
+
try {
|
|
523
|
+
prepared = await session.request("textDocument/prepareRename", { textDocument, position: pos })
|
|
524
|
+
} catch {}
|
|
525
|
+
const result = await session.request("textDocument/rename", { textDocument, position: pos, newName })
|
|
526
|
+
return {
|
|
527
|
+
prepared: prepared || null,
|
|
528
|
+
newName,
|
|
529
|
+
...sanitizeWorkspaceEdit(session.base, result, maxResults),
|
|
530
|
+
applied: false,
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (op === "incoming-calls" || op === "outgoing-calls") {
|
|
534
|
+
const prepared = await session.request("textDocument/prepareCallHierarchy", { textDocument, position: pos })
|
|
535
|
+
const item = Array.isArray(prepared) ? prepared[0] : prepared
|
|
536
|
+
if (!item) return { root: null, calls: [] }
|
|
537
|
+
const method = op === "incoming-calls" ? "callHierarchy/incomingCalls" : "callHierarchy/outgoingCalls"
|
|
538
|
+
const result = await session.request(method, { item })
|
|
539
|
+
const rootItem = sanitizeHierarchyItem(session.base, item)
|
|
540
|
+
if (rootItem) delete rootItem._raw
|
|
541
|
+
return {
|
|
542
|
+
root: rootItem,
|
|
543
|
+
calls: sanitizeHierarchyCalls(session.base, result, op === "incoming-calls" ? "incoming" : "outgoing", maxResults),
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return null
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const persistence = lspPersistencePolicy(options)
|
|
550
|
+
const managed = await withManagedLspSession(
|
|
551
|
+
target,
|
|
552
|
+
provider,
|
|
553
|
+
{ ...options, persistent: persistence.enabled, operation: op },
|
|
554
|
+
execute,
|
|
555
|
+
).catch((error) => ({
|
|
556
|
+
ok: false,
|
|
557
|
+
reason: "managed-lsp-exception",
|
|
558
|
+
error: String(error instanceof Error ? error.message : error).slice(0, 1200),
|
|
559
|
+
}))
|
|
560
|
+
|
|
561
|
+
if (managed?.ok) {
|
|
562
|
+
return {
|
|
563
|
+
schemaVersion: 2,
|
|
564
|
+
file: relativeFile,
|
|
565
|
+
available: true,
|
|
566
|
+
provider: provider.id,
|
|
567
|
+
command: provider.command,
|
|
568
|
+
operation: op,
|
|
569
|
+
reason: "ok",
|
|
570
|
+
result: managed.result,
|
|
571
|
+
stderr: managed.stderr || null,
|
|
572
|
+
error: null,
|
|
573
|
+
persistent: true,
|
|
574
|
+
pool: { ...managed.meta, policy: persistence },
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const fallback = await withEphemeralLspSession(root, relative, { ...options, operation: op }, execute)
|
|
579
|
+
return {
|
|
580
|
+
...fallback,
|
|
581
|
+
persistent: false,
|
|
582
|
+
fallbackFrom: managed?.reason || "managed-lsp-unavailable",
|
|
583
|
+
managedError: managed?.error || null,
|
|
584
|
+
persistencePolicy: persistence,
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export function lspPoolStatus(options = {}) {
|
|
589
|
+
return managedLspPoolStatus(options)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export async function shutdownLspPool(root = null) {
|
|
593
|
+
return shutdownManagedLspPool(root)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export async function diagnoseCode(root, relative, options = {}) {
|
|
597
|
+
const result = await lspOperation(root, relative, "diagnostics", options)
|
|
598
|
+
return {
|
|
599
|
+
...result,
|
|
600
|
+
diagnostics: result?.result?.diagnostics || [],
|
|
601
|
+
result: undefined,
|
|
602
|
+
}
|
|
603
|
+
}
|