weavatrix 0.2.13 → 0.2.15
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 +99 -39
- package/SECURITY.md +2 -2
- package/docs/releases/v0.2.15.md +37 -0
- package/package.json +2 -2
- package/skill/SKILL.md +57 -38
- package/src/analysis/architecture/contract-graph.js +119 -0
- package/src/analysis/architecture/contract-schema.js +110 -0
- package/src/analysis/architecture/contract-storage.js +35 -0
- package/src/analysis/architecture/contract-verification.js +168 -0
- package/src/analysis/architecture-contract.js +16 -343
- package/src/analysis/change-classification/diff-parser.js +103 -0
- package/src/analysis/change-classification/options.js +40 -0
- package/src/analysis/change-classification/symbol-classifier.js +177 -0
- package/src/analysis/change-classification.js +120 -519
- package/src/analysis/dead-code-review/policy.js +23 -0
- package/src/analysis/dead-code-review.js +9 -19
- package/src/analysis/dep-check.js +14 -71
- package/src/analysis/dep-rules.js +10 -303
- package/src/analysis/dependency/conventions.js +16 -0
- package/src/analysis/dependency/scoped-dependencies.js +45 -0
- package/src/analysis/dependency/source-references.js +21 -0
- package/src/analysis/duplicates.tokenize.js +12 -2
- package/src/analysis/endpoints/common.js +62 -0
- package/src/analysis/endpoints/extract.js +107 -0
- package/src/analysis/endpoints/inventory.js +84 -0
- package/src/analysis/endpoints/mounts.js +129 -0
- package/src/analysis/endpoints-java.js +80 -3
- package/src/analysis/endpoints.js +3 -448
- package/src/analysis/git-history/analytics.js +177 -0
- package/src/analysis/git-history/collector.js +109 -0
- package/src/analysis/git-history/options.js +68 -0
- package/src/analysis/git-history.js +128 -577
- package/src/analysis/health-capabilities.js +82 -0
- package/src/analysis/http-contracts/analysis.js +169 -0
- package/src/analysis/http-contracts/client-call-detection.js +81 -0
- package/src/analysis/http-contracts/client-call-parser.js +255 -0
- package/src/analysis/http-contracts/graph-context.js +143 -0
- package/src/analysis/http-contracts/matching.js +61 -0
- package/src/analysis/http-contracts/shared.js +86 -0
- package/src/analysis/http-contracts.js +6 -822
- package/src/analysis/internal-audit/dependency-health.js +111 -0
- package/src/analysis/internal-audit/python-manifests.js +65 -0
- package/src/analysis/internal-audit/repo-files.js +55 -0
- package/src/analysis/internal-audit/supply-chain.js +132 -0
- package/src/analysis/internal-audit.collect.js +5 -106
- package/src/analysis/internal-audit.reach.js +20 -0
- package/src/analysis/internal-audit.run.js +114 -200
- package/src/analysis/jvm-dependency-evidence.js +69 -0
- package/src/analysis/source-correctness/retry-patterns.js +93 -0
- package/src/analysis/source-correctness.js +225 -0
- package/src/analysis/structure/dependency-graph.js +156 -0
- package/src/analysis/structure/findings.js +142 -0
- package/src/analysis/task-retrieval.js +3 -14
- package/src/graph/builder/go-receiver-resolution.js +112 -0
- package/src/graph/builder/js/queries.js +48 -0
- package/src/graph/builder/lang-go.js +89 -4
- package/src/graph/builder/lang-js.js +4 -44
- package/src/graph/builder/pass2-resolution.js +68 -0
- package/src/graph/freshness-probe.js +2 -1
- package/src/graph/incremental-refresh.js +3 -2
- package/src/graph/internal-builder.build.js +22 -183
- package/src/graph/internal-builder.pass2.js +161 -0
- package/src/graph/internal-builder.resolvers.js +2 -105
- package/src/graph/resolvers/rust.js +117 -0
- package/src/mcp/actions/advisories.mjs +18 -0
- package/src/mcp/actions/graph-lifecycle.mjs +148 -0
- package/src/mcp/actions/graph-sync.mjs +195 -0
- package/src/mcp/actions/hosted-architecture.mjs +57 -0
- package/src/mcp/architecture-bootstrap.mjs +168 -0
- package/src/mcp/architecture-starter.mjs +234 -0
- package/src/mcp/catalog.mjs +20 -6
- package/src/mcp/evidence/bun-lock-graph.mjs +209 -0
- package/src/mcp/evidence/duplicate-member-order.mjs +8 -0
- package/src/mcp/evidence/package-graph-common.mjs +115 -0
- package/src/mcp/evidence/package-lock-graph.mjs +92 -0
- package/src/mcp/evidence-snapshot.duplicates.mjs +3 -7
- package/src/mcp/evidence-snapshot.package-graph.mjs +5 -474
- package/src/mcp/git-output.mjs +10 -0
- package/src/mcp/graph/context-core.mjs +111 -0
- package/src/mcp/graph/context-seeds.mjs +197 -0
- package/src/mcp/graph/context-state.mjs +86 -0
- package/src/mcp/graph/reverse-reach.mjs +42 -0
- package/src/mcp/graph/tools-core.mjs +143 -0
- package/src/mcp/graph/tools-query.mjs +223 -0
- package/src/mcp/graph-context.mjs +4 -496
- package/src/mcp/health/audit-format.mjs +172 -0
- package/src/mcp/health/audit.mjs +171 -0
- package/src/mcp/health/dead-code.mjs +110 -0
- package/src/mcp/health/duplicates.mjs +83 -0
- package/src/mcp/health/endpoints.mjs +42 -0
- package/src/mcp/health/structure.mjs +219 -0
- package/src/mcp/runtime-version.mjs +32 -0
- package/src/mcp/server/auto-refresh.mjs +66 -0
- package/src/mcp/server/runtime-config.mjs +43 -0
- package/src/mcp/server/shutdown.mjs +40 -0
- package/src/mcp/sync/evidence-architecture.mjs +54 -0
- package/src/mcp/sync/evidence-common.mjs +88 -0
- package/src/mcp/sync/evidence-duplicates.mjs +97 -0
- package/src/mcp/sync/evidence-health.mjs +56 -0
- package/src/mcp/sync/evidence-packages.mjs +95 -0
- package/src/mcp/sync/payload-common.mjs +97 -0
- package/src/mcp/sync/payload-v2.mjs +53 -0
- package/src/mcp/sync/payload-v3.mjs +79 -0
- package/src/mcp/sync-evidence.mjs +7 -402
- package/src/mcp/sync-payload.mjs +10 -244
- package/src/mcp/tools-actions.mjs +18 -414
- package/src/mcp/tools-architecture.mjs +18 -70
- package/src/mcp/tools-context.mjs +56 -15
- package/src/mcp/tools-endpoints.mjs +4 -1
- package/src/mcp/tools-graph.mjs +17 -331
- package/src/mcp/tools-health.mjs +24 -705
- package/src/mcp/tools-impact-change.mjs +2 -38
- package/src/mcp/tools-impact.mjs +2 -43
- package/src/mcp-server.mjs +35 -146
- package/src/path-classification.js +14 -0
- package/src/precision/lsp-client/constants.js +12 -0
- package/src/precision/lsp-client/environment.js +20 -0
- package/src/precision/lsp-client/errors.js +15 -0
- package/src/precision/lsp-client/lifecycle.js +127 -0
- package/src/precision/lsp-client/message-parser.js +81 -0
- package/src/precision/lsp-client/protocol.js +212 -0
- package/src/precision/lsp-client/registry.js +58 -0
- package/src/precision/lsp-client/repo-uri.js +60 -0
- package/src/precision/lsp-client/stdio-client.js +151 -0
- package/src/precision/lsp-client.js +10 -682
- package/src/precision/lsp-overlay/build.js +238 -0
- package/src/precision/lsp-overlay/contract.js +61 -0
- package/src/precision/lsp-overlay/reference-results.js +108 -0
- package/src/precision/lsp-overlay/semantic-inputs.js +62 -0
- package/src/precision/lsp-overlay/source-session.js +134 -0
- package/src/precision/lsp-overlay/store.js +150 -0
- package/src/precision/lsp-overlay/target-index.js +147 -0
- package/src/precision/lsp-overlay/target-query.js +55 -0
- package/src/precision/lsp-overlay.js +11 -868
- package/src/precision/typescript-lsp-provider.js +12 -682
- package/src/precision/typescript-provider/client.js +69 -0
- package/src/precision/typescript-provider/discovery.js +124 -0
- package/src/precision/typescript-provider/project-host.js +249 -0
- package/src/precision/typescript-provider/project-safety.js +161 -0
- package/src/precision/typescript-provider/reference-usage.js +53 -0
- package/src/security/malware-heuristics.sweep.js +1 -1
- package/src/security/registry-sig.classify.js +2 -1
- package/src/security/registry-sig.rules.js +3 -3
- package/src/version.js +7 -0
- package/docs/releases/v0.2.13.md +0 -48
|
@@ -1,682 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const JSON_RPC_VERSION = '2.0'
|
|
12
|
-
const HEADER_DELIMITER = Buffer.from('\r\n\r\n', 'ascii')
|
|
13
|
-
const UTF8_DECODER = new TextDecoder('utf-8', {fatal: true})
|
|
14
|
-
const ACTIVE_LSP_CLIENTS = new Set()
|
|
15
|
-
let ACCEPTING_LSP_CLIENTS = true
|
|
16
|
-
const LSP_ENV_ALLOWLIST = new Set([
|
|
17
|
-
'path', 'pathext', 'systemroot', 'windir', 'comspec',
|
|
18
|
-
'temp', 'tmp', 'tmpdir', 'home', 'userprofile', 'localappdata', 'appdata',
|
|
19
|
-
'lang', 'language', 'lc_all', 'lc_ctype',
|
|
20
|
-
])
|
|
21
|
-
|
|
22
|
-
export function lspChildProcessEnv(overrides = {}) {
|
|
23
|
-
const inherited = childProcessEnv(overrides)
|
|
24
|
-
const clean = Object.fromEntries(Object.entries(inherited).filter(([key]) => LSP_ENV_ALLOWLIST.has(key.toLowerCase())))
|
|
25
|
-
const safePath = [dirname(process.execPath)]
|
|
26
|
-
const systemRoot = inherited.SystemRoot || inherited.SYSTEMROOT || inherited.WINDIR
|
|
27
|
-
if (process.platform === 'win32' && systemRoot) safePath.push(join(systemRoot, 'System32'))
|
|
28
|
-
clean.PATH = [...new Set(safePath)].join(delimiter)
|
|
29
|
-
return clean
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function activeLspClientCount() {
|
|
33
|
-
return ACTIVE_LSP_CLIENTS.size
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// A graph build can reach the precision phase after MCP stdin has already closed. Flip this gate
|
|
37
|
-
// synchronously at shutdown start so no new TLS/tsserver tree can appear after the active set was
|
|
38
|
-
// drained. The MCP process is terminal after this transition, so it is intentionally irreversible.
|
|
39
|
-
export function beginLspClientShutdown() {
|
|
40
|
-
ACCEPTING_LSP_CLIENTS = false
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// MCP disconnects can arrive while an automatic graph refresh is still asking tsserver for
|
|
44
|
-
// references. Keep process ownership explicit so the stdio shell can drain or terminate every
|
|
45
|
-
// bundled semantic-provider tree before its own process exits.
|
|
46
|
-
export async function shutdownActiveLspClients({timeoutMs = 3_000} = {}) {
|
|
47
|
-
const boundedTimeout = Math.max(250, Math.min(10_000, Number(timeoutMs) || 3_000))
|
|
48
|
-
const startedAt = Date.now()
|
|
49
|
-
const clients = [...ACTIVE_LSP_CLIENTS]
|
|
50
|
-
if (!clients.length) return {requested: 0, remaining: 0, timedOut: false}
|
|
51
|
-
|
|
52
|
-
let timer
|
|
53
|
-
const gracefulBudget = Math.max(125, Math.floor(boundedTimeout * 0.6))
|
|
54
|
-
const graceful = Promise.allSettled(clients.map((client) => client.shutdown({timeoutMs: boundedTimeout})))
|
|
55
|
-
const outcome = await Promise.race([
|
|
56
|
-
graceful.then(() => 'closed'),
|
|
57
|
-
new Promise((resolveOutcome) => { timer = setTimeout(() => resolveOutcome('timeout'), gracefulBudget) }),
|
|
58
|
-
])
|
|
59
|
-
if (timer) clearTimeout(timer)
|
|
60
|
-
|
|
61
|
-
const survivors = [...ACTIVE_LSP_CLIENTS]
|
|
62
|
-
if (survivors.length) {
|
|
63
|
-
const reason = new Error(`MCP shutdown timed out with ${ACTIVE_LSP_CLIENTS.size} active LSP client(s)`)
|
|
64
|
-
const forceBudget = Math.max(100, boundedTimeout - (Date.now() - startedAt))
|
|
65
|
-
// On Windows, wait for taskkill /T /F itself before exiting the MCP parent. Otherwise the TLS
|
|
66
|
-
// process can die first while its tsserver child keeps the repository directory open.
|
|
67
|
-
await Promise.allSettled(survivors.map(async (client) => {
|
|
68
|
-
if (typeof client.killWindowsTreeAndWait === 'function') {
|
|
69
|
-
await client.killWindowsTreeAndWait(forceBudget)
|
|
70
|
-
}
|
|
71
|
-
client.kill(reason)
|
|
72
|
-
if (typeof client.waitForExit === 'function') await client.waitForExit(Math.max(100, boundedTimeout - (Date.now() - startedAt)))
|
|
73
|
-
}))
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
requested: clients.length,
|
|
78
|
-
remaining: ACTIVE_LSP_CLIENTS.size,
|
|
79
|
-
timedOut: outcome === 'timeout' || ACTIVE_LSP_CLIENTS.size > 0,
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export class LspProtocolError extends Error {
|
|
84
|
-
constructor(message, options) {
|
|
85
|
-
super(message, options)
|
|
86
|
-
this.name = 'LspProtocolError'
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export class LspTimeoutError extends Error {
|
|
91
|
-
constructor(method, timeoutMs) {
|
|
92
|
-
super(`LSP request timed out after ${timeoutMs}ms: ${method}`)
|
|
93
|
-
this.name = 'LspTimeoutError'
|
|
94
|
-
this.method = method
|
|
95
|
-
this.timeoutMs = timeoutMs
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function positiveInteger(value, fallback, label) {
|
|
100
|
-
const candidate = value == null ? fallback : Number(value)
|
|
101
|
-
if (!Number.isSafeInteger(candidate) || candidate <= 0) throw new TypeError(`${label} must be a positive integer`)
|
|
102
|
-
return candidate
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function asBuffer(chunk) {
|
|
106
|
-
if (Buffer.isBuffer(chunk)) return chunk
|
|
107
|
-
if (chunk instanceof Uint8Array) return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength)
|
|
108
|
-
return Buffer.from(chunk)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function parseHeaders(bytes, maxMessageBytes) {
|
|
112
|
-
const text = bytes.toString('ascii')
|
|
113
|
-
const lines = text.split('\r\n')
|
|
114
|
-
let contentLength = null
|
|
115
|
-
for (const line of lines) {
|
|
116
|
-
const match = /^([!#$%&'*+\-.^_`|~0-9A-Za-z]+):[ \t]*(.*)$/.exec(line)
|
|
117
|
-
if (!match) throw new LspProtocolError('Malformed LSP header line')
|
|
118
|
-
const name = match[1].toLowerCase()
|
|
119
|
-
if (name !== 'content-length') continue
|
|
120
|
-
if (contentLength != null) throw new LspProtocolError('Duplicate Content-Length header')
|
|
121
|
-
if (!/^(0|[1-9][0-9]*)$/.test(match[2])) throw new LspProtocolError('Invalid Content-Length header')
|
|
122
|
-
contentLength = Number(match[2])
|
|
123
|
-
if (!Number.isSafeInteger(contentLength) || contentLength <= 0) throw new LspProtocolError('Content-Length must be positive')
|
|
124
|
-
if (contentLength > maxMessageBytes) {
|
|
125
|
-
throw new LspProtocolError(`LSP message exceeds ${maxMessageBytes} byte limit`)
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
if (contentLength == null) throw new LspProtocolError('Missing Content-Length header')
|
|
129
|
-
return contentLength
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** Incremental parser for the Content-Length framed JSON-RPC transport used by LSP. */
|
|
133
|
-
export class ContentLengthMessageParser {
|
|
134
|
-
constructor({onMessage, maxMessageBytes = DEFAULT_MAX_MESSAGE_BYTES, maxHeaderBytes = DEFAULT_MAX_HEADER_BYTES} = {}) {
|
|
135
|
-
if (typeof onMessage !== 'function') throw new TypeError('onMessage must be a function')
|
|
136
|
-
this.onMessage = onMessage
|
|
137
|
-
this.maxMessageBytes = positiveInteger(maxMessageBytes, DEFAULT_MAX_MESSAGE_BYTES, 'maxMessageBytes')
|
|
138
|
-
this.maxHeaderBytes = positiveInteger(maxHeaderBytes, DEFAULT_MAX_HEADER_BYTES, 'maxHeaderBytes')
|
|
139
|
-
this.buffer = Buffer.alloc(0)
|
|
140
|
-
this.expectedBodyBytes = null
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
push(chunk) {
|
|
144
|
-
const incoming = asBuffer(chunk)
|
|
145
|
-
if (incoming.length === 0) return
|
|
146
|
-
this.buffer = this.buffer.length === 0 ? incoming : Buffer.concat([this.buffer, incoming])
|
|
147
|
-
|
|
148
|
-
while (this.buffer.length > 0) {
|
|
149
|
-
if (this.expectedBodyBytes == null) {
|
|
150
|
-
const delimiterIndex = this.buffer.indexOf(HEADER_DELIMITER)
|
|
151
|
-
if (delimiterIndex < 0) {
|
|
152
|
-
if (this.buffer.length > this.maxHeaderBytes) throw new LspProtocolError('LSP header exceeds byte limit')
|
|
153
|
-
return
|
|
154
|
-
}
|
|
155
|
-
if (delimiterIndex === 0) throw new LspProtocolError('Empty LSP header block')
|
|
156
|
-
if (delimiterIndex > this.maxHeaderBytes) throw new LspProtocolError('LSP header exceeds byte limit')
|
|
157
|
-
this.expectedBodyBytes = parseHeaders(this.buffer.subarray(0, delimiterIndex), this.maxMessageBytes)
|
|
158
|
-
this.buffer = this.buffer.subarray(delimiterIndex + HEADER_DELIMITER.length)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (this.buffer.length < this.expectedBodyBytes) return
|
|
162
|
-
const body = this.buffer.subarray(0, this.expectedBodyBytes)
|
|
163
|
-
this.buffer = this.buffer.subarray(this.expectedBodyBytes)
|
|
164
|
-
this.expectedBodyBytes = null
|
|
165
|
-
|
|
166
|
-
let value
|
|
167
|
-
try {
|
|
168
|
-
value = JSON.parse(UTF8_DECODER.decode(body))
|
|
169
|
-
} catch (error) {
|
|
170
|
-
throw new LspProtocolError('Invalid UTF-8 JSON in LSP message', {cause: error})
|
|
171
|
-
}
|
|
172
|
-
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
|
|
173
|
-
throw new LspProtocolError('LSP message must be a JSON object')
|
|
174
|
-
}
|
|
175
|
-
this.onMessage(value)
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function normalizeFilesystemPath(path) {
|
|
181
|
-
let normalized = path
|
|
182
|
-
if (process.platform === 'win32' && normalized.startsWith('\\\\?\\UNC\\')) normalized = `\\\\${normalized.slice(8)}`
|
|
183
|
-
else if (process.platform === 'win32' && normalized.startsWith('\\\\?\\')) normalized = normalized.slice(4)
|
|
184
|
-
return resolve(normalized)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function realpathIfPossible(path) {
|
|
188
|
-
try {
|
|
189
|
-
return normalizeFilesystemPath(realpathSync.native(path))
|
|
190
|
-
} catch {
|
|
191
|
-
return normalizeFilesystemPath(path)
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function existingRealpath(path) {
|
|
196
|
-
try {
|
|
197
|
-
return normalizeFilesystemPath(realpathSync.native(path))
|
|
198
|
-
} catch {
|
|
199
|
-
return null
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function pathInside(rootPath, candidatePath) {
|
|
204
|
-
const rel = relative(rootPath, candidatePath)
|
|
205
|
-
return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel))
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function portableRelative(rootPath, candidatePath) {
|
|
209
|
-
return relative(rootPath, candidatePath).split(sep).join('/')
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Creates the only path/URI conversion surface used by the client. Existing symlinks are
|
|
214
|
-
* canonicalized before the repository-boundary check, so an in-repo symlink cannot expose files
|
|
215
|
-
* outside the repository through definition/reference results.
|
|
216
|
-
*/
|
|
217
|
-
export function createRepoUriNormalizer(repoRoot) {
|
|
218
|
-
if (typeof repoRoot !== 'string' || repoRoot.trim() === '') throw new TypeError('repoRoot is required')
|
|
219
|
-
const lexicalRoot = normalizeFilesystemPath(resolve(repoRoot))
|
|
220
|
-
const absoluteRoot = realpathIfPossible(lexicalRoot)
|
|
221
|
-
|
|
222
|
-
const toAbsolute = (filePath) => {
|
|
223
|
-
if (typeof filePath !== 'string' || filePath.trim() === '') throw new TypeError('filePath is required')
|
|
224
|
-
const lexicalPath = normalizeFilesystemPath(resolve(lexicalRoot, filePath))
|
|
225
|
-
const canonicalPath = existingRealpath(lexicalPath)
|
|
226
|
-
if (canonicalPath == null) {
|
|
227
|
-
if (!pathInside(lexicalRoot, lexicalPath)) throw new RangeError('LSP path is outside the repository')
|
|
228
|
-
return lexicalPath
|
|
229
|
-
}
|
|
230
|
-
if (!pathInside(absoluteRoot, canonicalPath)) throw new RangeError('LSP path resolves outside the repository')
|
|
231
|
-
return canonicalPath
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const fromUri = (uri) => {
|
|
235
|
-
if (typeof uri !== 'string' || !uri.startsWith('file:')) throw new RangeError('Only file: LSP URIs are accepted')
|
|
236
|
-
let filePath
|
|
237
|
-
try {
|
|
238
|
-
filePath = fileURLToPath(uri)
|
|
239
|
-
} catch (error) {
|
|
240
|
-
throw new RangeError('Invalid file: LSP URI', {cause: error})
|
|
241
|
-
}
|
|
242
|
-
const absolutePath = toAbsolute(filePath)
|
|
243
|
-
return {file: portableRelative(absoluteRoot, absolutePath), absolutePath, uri: pathToFileURL(absolutePath).href}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const toUri = (filePath) => {
|
|
247
|
-
const absolutePath = toAbsolute(filePath)
|
|
248
|
-
return {file: portableRelative(absoluteRoot, absolutePath), absolutePath, uri: pathToFileURL(absolutePath).href}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return {rootPath: absoluteRoot, rootUri: pathToFileURL(absoluteRoot).href, toAbsolute, toUri, fromUri}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function assertPosition(position) {
|
|
255
|
-
if (position == null || typeof position !== 'object') throw new TypeError('position is required')
|
|
256
|
-
const line = Number(position.line)
|
|
257
|
-
const character = Number(position.character)
|
|
258
|
-
if (!Number.isSafeInteger(line) || line < 0 || !Number.isSafeInteger(character) || character < 0) {
|
|
259
|
-
throw new TypeError('position line and character must be non-negative integers')
|
|
260
|
-
}
|
|
261
|
-
return {line, character}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function normalizeRange(range) {
|
|
265
|
-
if (range == null || typeof range !== 'object') throw new LspProtocolError('LSP location is missing a range')
|
|
266
|
-
return {start: assertPosition(range.start), end: assertPosition(range.end)}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function jsonRpcError(error) {
|
|
270
|
-
const message = error?.message || 'LSP request failed'
|
|
271
|
-
const wrapped = new Error(message)
|
|
272
|
-
wrapped.name = 'LspResponseError'
|
|
273
|
-
wrapped.code = error?.code
|
|
274
|
-
wrapped.data = error?.data
|
|
275
|
-
return wrapped
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function locationArray(result) {
|
|
279
|
-
if (result == null) return []
|
|
280
|
-
return Array.isArray(result) ? result : [result]
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export class StdioLspClient {
|
|
284
|
-
constructor({
|
|
285
|
-
repoRoot,
|
|
286
|
-
executablePath = process.execPath,
|
|
287
|
-
args = [],
|
|
288
|
-
env = {},
|
|
289
|
-
requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
|
|
290
|
-
maxMessageBytes = DEFAULT_MAX_MESSAGE_BYTES,
|
|
291
|
-
maxHeaderBytes = DEFAULT_MAX_HEADER_BYTES,
|
|
292
|
-
onNotification,
|
|
293
|
-
onServerRequest,
|
|
294
|
-
spawn = spawnChild,
|
|
295
|
-
} = {}) {
|
|
296
|
-
if (typeof executablePath !== 'string' || !isAbsolute(executablePath)) {
|
|
297
|
-
throw new TypeError('LSP executablePath must be absolute')
|
|
298
|
-
}
|
|
299
|
-
if (!Array.isArray(args) || args.some((arg) => typeof arg !== 'string')) throw new TypeError('LSP args must be strings')
|
|
300
|
-
if (typeof spawn !== 'function') throw new TypeError('spawn must be a function')
|
|
301
|
-
if (!ACCEPTING_LSP_CLIENTS) throw new Error('LSP process creation is disabled during MCP shutdown')
|
|
302
|
-
this.normalizer = createRepoUriNormalizer(repoRoot)
|
|
303
|
-
this.requestTimeoutMs = positiveInteger(requestTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS, 'requestTimeoutMs')
|
|
304
|
-
this.maxMessageBytes = positiveInteger(maxMessageBytes, DEFAULT_MAX_MESSAGE_BYTES, 'maxMessageBytes')
|
|
305
|
-
this.onNotification = typeof onNotification === 'function' ? onNotification : null
|
|
306
|
-
this.onServerRequest = typeof onServerRequest === 'function' ? onServerRequest : null
|
|
307
|
-
this.pending = new Map()
|
|
308
|
-
this.openDocuments = new Map()
|
|
309
|
-
this.nextId = 1
|
|
310
|
-
this.state = 'starting'
|
|
311
|
-
this.stderrTail = ''
|
|
312
|
-
this.shutdownPromise = null
|
|
313
|
-
|
|
314
|
-
this.child = spawn(executablePath, args, {
|
|
315
|
-
cwd: this.normalizer.rootPath,
|
|
316
|
-
// The semantic provider needs only OS/process basics. Do not expose package-registry,
|
|
317
|
-
// cloud, hosted-sync, proxy, API-key, NODE_OPTIONS, or arbitrary repo-shell environment.
|
|
318
|
-
env: lspChildProcessEnv(env),
|
|
319
|
-
shell: false,
|
|
320
|
-
// On POSIX, make TLS the leader of a private process group so forced cleanup can also
|
|
321
|
-
// terminate its tsserver child. Piped stdio stays owned by this client; we never unref.
|
|
322
|
-
detached: process.platform !== 'win32',
|
|
323
|
-
windowsHide: true,
|
|
324
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
325
|
-
})
|
|
326
|
-
this.processGroupPid = process.platform !== 'win32' && Number.isInteger(this.child.pid)
|
|
327
|
-
? this.child.pid : null
|
|
328
|
-
ACTIVE_LSP_CLIENTS.add(this)
|
|
329
|
-
this.parser = new ContentLengthMessageParser({
|
|
330
|
-
maxMessageBytes: this.maxMessageBytes,
|
|
331
|
-
maxHeaderBytes,
|
|
332
|
-
onMessage: (message) => this.handleMessage(message),
|
|
333
|
-
})
|
|
334
|
-
this.child.stdout?.on('data', (chunk) => {
|
|
335
|
-
try {
|
|
336
|
-
this.parser.push(chunk)
|
|
337
|
-
} catch (error) {
|
|
338
|
-
this.fail(error)
|
|
339
|
-
}
|
|
340
|
-
})
|
|
341
|
-
this.child.stderr?.setEncoding('utf8')
|
|
342
|
-
this.child.stderr?.on('data', (chunk) => {
|
|
343
|
-
this.stderrTail = `${this.stderrTail}${chunk}`.slice(-16_000)
|
|
344
|
-
})
|
|
345
|
-
this.spawned = new Promise((resolveSpawn, rejectSpawn) => {
|
|
346
|
-
this.child.once('spawn', () => {
|
|
347
|
-
if (this.state === 'starting') this.state = 'running'
|
|
348
|
-
resolveSpawn(this)
|
|
349
|
-
})
|
|
350
|
-
this.child.once('error', (error) => {
|
|
351
|
-
this.fail(error)
|
|
352
|
-
rejectSpawn(error)
|
|
353
|
-
})
|
|
354
|
-
})
|
|
355
|
-
this.exited = new Promise((resolveExit) => {
|
|
356
|
-
let settled = false
|
|
357
|
-
const finishExit = (code, signal) => {
|
|
358
|
-
if (settled) return
|
|
359
|
-
settled = true
|
|
360
|
-
const wasStopping = this.state === 'stopping' || this.state === 'closed'
|
|
361
|
-
this.state = 'closed'
|
|
362
|
-
const suffix = this.stderrTail ? `\n${this.stderrTail}` : ''
|
|
363
|
-
this.rejectPending(new Error(`LSP server exited (code=${code}, signal=${signal})${suffix}`))
|
|
364
|
-
if (!wasStopping && code !== 0) this.openDocuments.clear()
|
|
365
|
-
ACTIVE_LSP_CLIENTS.delete(this)
|
|
366
|
-
resolveExit({code, signal})
|
|
367
|
-
}
|
|
368
|
-
// spawn failures emit error + close without an exit event. Listen to both so the process
|
|
369
|
-
// registry cannot retain a client that never started.
|
|
370
|
-
this.child.once('exit', finishExit)
|
|
371
|
-
this.child.once('close', finishExit)
|
|
372
|
-
})
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
async start() {
|
|
376
|
-
await this.spawned
|
|
377
|
-
return this
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
assertWritable() {
|
|
381
|
-
if (!['running', 'initialized', 'stopping'].includes(this.state) || this.child.stdin?.destroyed) {
|
|
382
|
-
throw new Error(`LSP client is not writable (state=${this.state})`)
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
writeMessage(message) {
|
|
387
|
-
this.assertWritable()
|
|
388
|
-
const body = Buffer.from(JSON.stringify(message), 'utf8')
|
|
389
|
-
if (body.length > this.maxMessageBytes) throw new LspProtocolError(`Outgoing LSP message exceeds ${this.maxMessageBytes} byte limit`)
|
|
390
|
-
const header = Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, 'ascii')
|
|
391
|
-
return new Promise((resolveWrite, rejectWrite) => {
|
|
392
|
-
this.child.stdin.write(Buffer.concat([header, body]), (error) => error ? rejectWrite(error) : resolveWrite())
|
|
393
|
-
})
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
async request(method, params = null, {timeoutMs = this.requestTimeoutMs} = {}) {
|
|
397
|
-
if (typeof method !== 'string' || method === '') throw new TypeError('LSP method is required')
|
|
398
|
-
const boundedTimeout = positiveInteger(timeoutMs, this.requestTimeoutMs, 'timeoutMs')
|
|
399
|
-
const id = this.nextId++
|
|
400
|
-
let entry
|
|
401
|
-
const response = new Promise((resolveResponse, rejectResponse) => {
|
|
402
|
-
const timer = setTimeout(() => {
|
|
403
|
-
if (this.pending.get(id) !== entry) return
|
|
404
|
-
this.pending.delete(id)
|
|
405
|
-
void this.writeMessage({jsonrpc: JSON_RPC_VERSION, method: '$/cancelRequest', params: {id}}).catch(() => {})
|
|
406
|
-
rejectResponse(new LspTimeoutError(method, boundedTimeout))
|
|
407
|
-
}, boundedTimeout)
|
|
408
|
-
entry = {method, timer, resolve: resolveResponse, reject: rejectResponse}
|
|
409
|
-
this.pending.set(id, entry)
|
|
410
|
-
})
|
|
411
|
-
try {
|
|
412
|
-
await this.writeMessage({jsonrpc: JSON_RPC_VERSION, id, method, params})
|
|
413
|
-
} catch (error) {
|
|
414
|
-
if (this.pending.get(id) === entry) {
|
|
415
|
-
clearTimeout(entry.timer)
|
|
416
|
-
this.pending.delete(id)
|
|
417
|
-
entry.reject(error)
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
return response
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
async notify(method, params = null) {
|
|
424
|
-
if (typeof method !== 'string' || method === '') throw new TypeError('LSP method is required')
|
|
425
|
-
await this.writeMessage({jsonrpc: JSON_RPC_VERSION, method, params})
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
async initialize({capabilities = {}, initializationOptions, clientInfo = {name: 'weavatrix', version: '0.2.6'}} = {}) {
|
|
429
|
-
if (this.state !== 'running') throw new Error(`LSP initialize is invalid in state=${this.state}`)
|
|
430
|
-
const result = await this.request('initialize', {
|
|
431
|
-
processId: process.pid,
|
|
432
|
-
clientInfo,
|
|
433
|
-
rootUri: this.normalizer.rootUri,
|
|
434
|
-
capabilities,
|
|
435
|
-
initializationOptions,
|
|
436
|
-
workspaceFolders: [{uri: this.normalizer.rootUri, name: this.normalizer.rootPath.split(/[\\/]/).pop() || 'repository'}],
|
|
437
|
-
})
|
|
438
|
-
await this.notify('initialized', {})
|
|
439
|
-
this.state = 'initialized'
|
|
440
|
-
return result
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
async openDocument({filePath, text, languageId, version = 1}) {
|
|
444
|
-
if (this.state !== 'initialized') throw new Error('LSP client is not initialized')
|
|
445
|
-
if (typeof text !== 'string') throw new TypeError('document text must be a string')
|
|
446
|
-
if (typeof languageId !== 'string' || languageId === '') throw new TypeError('languageId is required')
|
|
447
|
-
if (!Number.isSafeInteger(version) || version < 0) throw new TypeError('document version must be a non-negative integer')
|
|
448
|
-
const normalized = this.normalizer.toUri(filePath)
|
|
449
|
-
await this.notify('textDocument/didOpen', {textDocument: {uri: normalized.uri, languageId, version, text}})
|
|
450
|
-
this.openDocuments.set(normalized.uri, {file: normalized.file, version, languageId})
|
|
451
|
-
return {file: normalized.file, version, languageId}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
async closeDocument(filePath) {
|
|
455
|
-
const normalized = this.normalizer.toUri(filePath)
|
|
456
|
-
if (!this.openDocuments.has(normalized.uri)) return false
|
|
457
|
-
await this.notify('textDocument/didClose', {textDocument: {uri: normalized.uri}})
|
|
458
|
-
this.openDocuments.delete(normalized.uri)
|
|
459
|
-
return true
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
normalizeLocations(result) {
|
|
463
|
-
const locations = []
|
|
464
|
-
for (const raw of locationArray(result)) {
|
|
465
|
-
if (raw == null || typeof raw !== 'object') continue
|
|
466
|
-
const uri = raw.uri || raw.targetUri
|
|
467
|
-
const range = raw.range || raw.targetSelectionRange || raw.targetRange
|
|
468
|
-
try {
|
|
469
|
-
const normalized = this.normalizer.fromUri(uri)
|
|
470
|
-
locations.push({file: normalized.file, range: normalizeRange(range)})
|
|
471
|
-
} catch (error) {
|
|
472
|
-
if (error instanceof RangeError) continue
|
|
473
|
-
throw error
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
return locations
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
async definition({filePath, position, timeoutMs}) {
|
|
480
|
-
const normalized = this.normalizer.toUri(filePath)
|
|
481
|
-
const result = await this.request('textDocument/definition', {
|
|
482
|
-
textDocument: {uri: normalized.uri},
|
|
483
|
-
position: assertPosition(position),
|
|
484
|
-
}, {timeoutMs: timeoutMs ?? this.requestTimeoutMs})
|
|
485
|
-
return this.normalizeLocations(result)
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
async references({filePath, position, includeDeclaration = true, timeoutMs}) {
|
|
489
|
-
const normalized = this.normalizer.toUri(filePath)
|
|
490
|
-
const result = await this.request('textDocument/references', {
|
|
491
|
-
textDocument: {uri: normalized.uri},
|
|
492
|
-
position: assertPosition(position),
|
|
493
|
-
context: {includeDeclaration: Boolean(includeDeclaration)},
|
|
494
|
-
}, {timeoutMs: timeoutMs ?? this.requestTimeoutMs})
|
|
495
|
-
return this.normalizeLocations(result)
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
async defaultServerRequest(method, params) {
|
|
499
|
-
if (method === 'workspace/configuration') {
|
|
500
|
-
return Array.isArray(params?.items) ? params.items.map(() => null) : []
|
|
501
|
-
}
|
|
502
|
-
if (method === 'workspace/workspaceFolders') {
|
|
503
|
-
return [{uri: this.normalizer.rootUri, name: this.normalizer.rootPath.split(/[\\/]/).pop() || 'repository'}]
|
|
504
|
-
}
|
|
505
|
-
if (method === 'client/registerCapability' || method === 'client/unregisterCapability' || method === 'window/workDoneProgress/create') return null
|
|
506
|
-
if (method === 'workspace/applyEdit') return {applied: false, failureReason: 'Weavatrix precision provider is read-only'}
|
|
507
|
-
const error = new Error(`Unsupported LSP server request: ${method}`)
|
|
508
|
-
error.code = -32601
|
|
509
|
-
throw error
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
async handleServerRequest(message) {
|
|
513
|
-
try {
|
|
514
|
-
const result = this.onServerRequest
|
|
515
|
-
? await this.onServerRequest(message.method, message.params, this)
|
|
516
|
-
: await this.defaultServerRequest(message.method, message.params)
|
|
517
|
-
await this.writeMessage({jsonrpc: JSON_RPC_VERSION, id: message.id, result: result ?? null})
|
|
518
|
-
} catch (error) {
|
|
519
|
-
try {
|
|
520
|
-
await this.writeMessage({
|
|
521
|
-
jsonrpc: JSON_RPC_VERSION,
|
|
522
|
-
id: message.id,
|
|
523
|
-
error: {code: Number.isInteger(error?.code) ? error.code : -32603, message: error?.message || 'LSP client error'},
|
|
524
|
-
})
|
|
525
|
-
} catch {
|
|
526
|
-
// The process may have exited while the request handler was running.
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
handleMessage(message) {
|
|
532
|
-
if (message.jsonrpc !== JSON_RPC_VERSION) throw new LspProtocolError('Unsupported JSON-RPC version from LSP server')
|
|
533
|
-
if (typeof message.method === 'string') {
|
|
534
|
-
if (message.id != null) void this.handleServerRequest(message)
|
|
535
|
-
else if (this.onNotification) this.onNotification(message.method, message.params, this)
|
|
536
|
-
return
|
|
537
|
-
}
|
|
538
|
-
if (message.id == null) throw new LspProtocolError('LSP response is missing an id')
|
|
539
|
-
const entry = this.pending.get(message.id)
|
|
540
|
-
if (!entry) return
|
|
541
|
-
this.pending.delete(message.id)
|
|
542
|
-
clearTimeout(entry.timer)
|
|
543
|
-
if (message.error != null) entry.reject(jsonRpcError(message.error))
|
|
544
|
-
else entry.resolve(message.result)
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
rejectPending(error) {
|
|
548
|
-
for (const entry of this.pending.values()) {
|
|
549
|
-
clearTimeout(entry.timer)
|
|
550
|
-
entry.reject(error)
|
|
551
|
-
}
|
|
552
|
-
this.pending.clear()
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
fail(error) {
|
|
556
|
-
if (this.state === 'closed' || this.state === 'failed') return
|
|
557
|
-
this.state = 'failed'
|
|
558
|
-
this.rejectPending(error)
|
|
559
|
-
this.kill(error)
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
kill(reason = new Error('LSP client was killed')) {
|
|
563
|
-
if (this.state !== 'closed') this.state = 'closed'
|
|
564
|
-
this.rejectPending(reason)
|
|
565
|
-
this.openDocuments.clear()
|
|
566
|
-
try { this.child.stdin?.destroy() } catch { /* already closed */ }
|
|
567
|
-
if (process.platform !== 'win32' && this.processGroupPid) {
|
|
568
|
-
try {
|
|
569
|
-
process.kill(-this.processGroupPid, 'SIGKILL')
|
|
570
|
-
return
|
|
571
|
-
} catch {
|
|
572
|
-
// A custom spawn implementation or an already-reaped group can make group kill
|
|
573
|
-
// unavailable. Fall through to the direct child handle as a bounded fallback.
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
if (this.child.exitCode != null || this.child.signalCode != null) return
|
|
577
|
-
if (process.platform === 'win32' && this.child.pid) {
|
|
578
|
-
// typescript-language-server owns a tsserver child. Kill the complete tree on timeout or
|
|
579
|
-
// protocol failure so reconnects never accumulate orphaned semantic providers.
|
|
580
|
-
try {
|
|
581
|
-
const killer = spawnChild('taskkill', ['/pid', String(this.child.pid), '/T', '/F'], {
|
|
582
|
-
shell: false, windowsHide: true, env: lspChildProcessEnv(), stdio: 'ignore',
|
|
583
|
-
})
|
|
584
|
-
const fallback = () => {
|
|
585
|
-
try { this.child.kill('SIGKILL') } catch { /* already exited */ }
|
|
586
|
-
}
|
|
587
|
-
killer.once('error', fallback)
|
|
588
|
-
// Sandboxed/locked-down Windows hosts can start taskkill but deny termination. Its
|
|
589
|
-
// non-zero exit must fall back to the direct child handle instead of looking successful.
|
|
590
|
-
killer.once('exit', (code) => { if (code !== 0) fallback() })
|
|
591
|
-
} catch {
|
|
592
|
-
try { this.child.kill('SIGKILL') } catch { /* already exited */ }
|
|
593
|
-
}
|
|
594
|
-
} else {
|
|
595
|
-
try { this.child.kill('SIGKILL') } catch { /* already exited */ }
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
async killWindowsTreeAndWait(timeoutMs = 3_000) {
|
|
600
|
-
if (process.platform !== 'win32' || !this.child.pid || this.child.exitCode != null) return
|
|
601
|
-
await new Promise((resolveKill) => {
|
|
602
|
-
let settled = false
|
|
603
|
-
const done = () => {
|
|
604
|
-
if (settled) return
|
|
605
|
-
settled = true
|
|
606
|
-
clearTimeout(timer)
|
|
607
|
-
resolveKill()
|
|
608
|
-
}
|
|
609
|
-
const timer = setTimeout(() => {
|
|
610
|
-
try { this.child.kill() } catch { /* already exited */ }
|
|
611
|
-
done()
|
|
612
|
-
}, Math.max(250, Math.min(5_000, Number(timeoutMs) || 3_000)))
|
|
613
|
-
try {
|
|
614
|
-
const killer = spawnChild('taskkill', ['/pid', String(this.child.pid), '/T', '/F'], {
|
|
615
|
-
shell: false, windowsHide: true, env: lspChildProcessEnv(), stdio: 'ignore',
|
|
616
|
-
})
|
|
617
|
-
const fallback = () => {
|
|
618
|
-
try { this.child.kill('SIGKILL') } catch { /* already exited */ }
|
|
619
|
-
}
|
|
620
|
-
killer.once('error', () => { fallback(); done() })
|
|
621
|
-
killer.once('exit', (code) => {
|
|
622
|
-
if (code !== 0) fallback()
|
|
623
|
-
done()
|
|
624
|
-
})
|
|
625
|
-
} catch {
|
|
626
|
-
try { this.child.kill('SIGKILL') } catch { /* already exited */ }
|
|
627
|
-
done()
|
|
628
|
-
}
|
|
629
|
-
})
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
async waitForExit(timeoutMs = this.requestTimeoutMs) {
|
|
633
|
-
if (!ACTIVE_LSP_CLIENTS.has(this)) return true
|
|
634
|
-
const boundedTimeout = Math.max(100, Math.min(10_000, Number(timeoutMs) || this.requestTimeoutMs))
|
|
635
|
-
let timer
|
|
636
|
-
const outcome = await Promise.race([
|
|
637
|
-
this.exited.then(() => true),
|
|
638
|
-
new Promise((resolveExit) => { timer = setTimeout(() => resolveExit(false), boundedTimeout) }),
|
|
639
|
-
])
|
|
640
|
-
if (timer) clearTimeout(timer)
|
|
641
|
-
return outcome
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
async shutdown(options = {}) {
|
|
645
|
-
if (this.shutdownPromise) return this.shutdownPromise
|
|
646
|
-
this.shutdownPromise = this.shutdownOnce(options)
|
|
647
|
-
return this.shutdownPromise
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
async shutdownOnce({timeoutMs = this.requestTimeoutMs} = {}) {
|
|
651
|
-
const boundedTimeout = positiveInteger(timeoutMs, this.requestTimeoutMs, 'timeoutMs')
|
|
652
|
-
try {
|
|
653
|
-
if (this.state === 'starting') await this.spawned
|
|
654
|
-
if (this.state === 'closed') {
|
|
655
|
-
await this.waitForExit(boundedTimeout)
|
|
656
|
-
return
|
|
657
|
-
}
|
|
658
|
-
if (this.state === 'initialized') await this.request('shutdown', null, {timeoutMs: boundedTimeout})
|
|
659
|
-
this.state = 'stopping'
|
|
660
|
-
await this.notify('exit', null)
|
|
661
|
-
// TLS owns a separate tsserver process. On Windows, wait for a whole-tree termination
|
|
662
|
-
// after the protocol shutdown so watched repositories are released before close resolves.
|
|
663
|
-
await this.killWindowsTreeAndWait(Math.min(3_000, boundedTimeout))
|
|
664
|
-
if (process.platform !== 'win32' && this.processGroupPid) {
|
|
665
|
-
// TLS may exit while its tsserver child remains alive. Give the protocol exit a
|
|
666
|
-
// short grace period, then reap the complete private process group unconditionally.
|
|
667
|
-
await this.waitForExit(Math.min(1_000, boundedTimeout))
|
|
668
|
-
try { process.kill(-this.processGroupPid, 'SIGKILL') } catch { /* group already exited */ }
|
|
669
|
-
}
|
|
670
|
-
if (!await this.waitForExit(boundedTimeout)) this.kill(new LspTimeoutError('exit', boundedTimeout))
|
|
671
|
-
} catch (error) {
|
|
672
|
-
this.kill(error)
|
|
673
|
-
await this.killWindowsTreeAndWait(Math.min(2_000, boundedTimeout))
|
|
674
|
-
await this.waitForExit(Math.min(2_000, boundedTimeout))
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
export async function startStdioLspClient(options) {
|
|
680
|
-
const client = new StdioLspClient(options)
|
|
681
|
-
return client.start()
|
|
682
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
activeLspClientCount,
|
|
3
|
+
beginLspClientShutdown,
|
|
4
|
+
shutdownActiveLspClients,
|
|
5
|
+
} from './lsp-client/registry.js'
|
|
6
|
+
export {lspChildProcessEnv} from './lsp-client/environment.js'
|
|
7
|
+
export {LspProtocolError, LspTimeoutError} from './lsp-client/errors.js'
|
|
8
|
+
export {ContentLengthMessageParser} from './lsp-client/message-parser.js'
|
|
9
|
+
export {createRepoUriNormalizer} from './lsp-client/repo-uri.js'
|
|
10
|
+
export {StdioLspClient, startStdioLspClient} from './lsp-client/stdio-client.js'
|