weavatrix 0.2.13 → 0.2.14

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.
Files changed (130) hide show
  1. package/README.md +83 -39
  2. package/SECURITY.md +2 -2
  3. package/docs/releases/v0.2.14.md +93 -0
  4. package/package.json +2 -2
  5. package/skill/SKILL.md +57 -38
  6. package/src/analysis/architecture/contract-graph.js +119 -0
  7. package/src/analysis/architecture/contract-schema.js +110 -0
  8. package/src/analysis/architecture/contract-storage.js +35 -0
  9. package/src/analysis/architecture/contract-verification.js +168 -0
  10. package/src/analysis/architecture-contract.js +16 -343
  11. package/src/analysis/change-classification/diff-parser.js +103 -0
  12. package/src/analysis/change-classification/options.js +40 -0
  13. package/src/analysis/change-classification/symbol-classifier.js +177 -0
  14. package/src/analysis/change-classification.js +120 -519
  15. package/src/analysis/dead-code-review/policy.js +23 -0
  16. package/src/analysis/dead-code-review.js +9 -19
  17. package/src/analysis/dep-check.js +10 -57
  18. package/src/analysis/dep-rules.js +10 -303
  19. package/src/analysis/dependency/scoped-dependencies.js +40 -0
  20. package/src/analysis/endpoints/common.js +62 -0
  21. package/src/analysis/endpoints/extract.js +107 -0
  22. package/src/analysis/endpoints/inventory.js +84 -0
  23. package/src/analysis/endpoints/mounts.js +129 -0
  24. package/src/analysis/endpoints-java.js +80 -3
  25. package/src/analysis/endpoints.js +3 -448
  26. package/src/analysis/git-history/analytics.js +177 -0
  27. package/src/analysis/git-history/collector.js +109 -0
  28. package/src/analysis/git-history/options.js +68 -0
  29. package/src/analysis/git-history.js +128 -577
  30. package/src/analysis/health-capabilities.js +82 -0
  31. package/src/analysis/http-contracts/analysis.js +169 -0
  32. package/src/analysis/http-contracts/client-call-detection.js +81 -0
  33. package/src/analysis/http-contracts/client-call-parser.js +255 -0
  34. package/src/analysis/http-contracts/graph-context.js +143 -0
  35. package/src/analysis/http-contracts/matching.js +61 -0
  36. package/src/analysis/http-contracts/shared.js +86 -0
  37. package/src/analysis/http-contracts.js +6 -822
  38. package/src/analysis/internal-audit/dependency-health.js +111 -0
  39. package/src/analysis/internal-audit/python-manifests.js +65 -0
  40. package/src/analysis/internal-audit/repo-files.js +55 -0
  41. package/src/analysis/internal-audit/supply-chain.js +132 -0
  42. package/src/analysis/internal-audit.collect.js +5 -106
  43. package/src/analysis/internal-audit.run.js +113 -200
  44. package/src/analysis/jvm-dependency-evidence.js +69 -0
  45. package/src/analysis/source-correctness/retry-patterns.js +93 -0
  46. package/src/analysis/source-correctness.js +225 -0
  47. package/src/analysis/structure/dependency-graph.js +156 -0
  48. package/src/analysis/structure/findings.js +142 -0
  49. package/src/graph/builder/go-receiver-resolution.js +112 -0
  50. package/src/graph/builder/js/queries.js +48 -0
  51. package/src/graph/builder/lang-go.js +89 -4
  52. package/src/graph/builder/lang-js.js +4 -44
  53. package/src/graph/builder/pass2-resolution.js +68 -0
  54. package/src/graph/freshness-probe.js +2 -1
  55. package/src/graph/incremental-refresh.js +3 -2
  56. package/src/graph/internal-builder.build.js +22 -183
  57. package/src/graph/internal-builder.pass2.js +161 -0
  58. package/src/graph/internal-builder.resolvers.js +2 -105
  59. package/src/graph/resolvers/rust.js +117 -0
  60. package/src/mcp/actions/advisories.mjs +18 -0
  61. package/src/mcp/actions/graph-lifecycle.mjs +148 -0
  62. package/src/mcp/actions/graph-sync.mjs +195 -0
  63. package/src/mcp/actions/hosted-architecture.mjs +57 -0
  64. package/src/mcp/architecture-bootstrap.mjs +168 -0
  65. package/src/mcp/architecture-starter.mjs +234 -0
  66. package/src/mcp/catalog.mjs +20 -6
  67. package/src/mcp/evidence/bun-lock-graph.mjs +209 -0
  68. package/src/mcp/evidence/package-graph-common.mjs +115 -0
  69. package/src/mcp/evidence/package-lock-graph.mjs +92 -0
  70. package/src/mcp/evidence-snapshot.package-graph.mjs +5 -474
  71. package/src/mcp/graph/context-core.mjs +111 -0
  72. package/src/mcp/graph/context-seeds.mjs +208 -0
  73. package/src/mcp/graph/context-state.mjs +86 -0
  74. package/src/mcp/graph/tools-core.mjs +143 -0
  75. package/src/mcp/graph/tools-query.mjs +223 -0
  76. package/src/mcp/graph-context.mjs +4 -496
  77. package/src/mcp/health/audit-format.mjs +172 -0
  78. package/src/mcp/health/audit.mjs +171 -0
  79. package/src/mcp/health/dead-code.mjs +110 -0
  80. package/src/mcp/health/duplicates.mjs +83 -0
  81. package/src/mcp/health/endpoints.mjs +42 -0
  82. package/src/mcp/health/structure.mjs +219 -0
  83. package/src/mcp/runtime-version.mjs +32 -0
  84. package/src/mcp/server/auto-refresh.mjs +66 -0
  85. package/src/mcp/server/runtime-config.mjs +43 -0
  86. package/src/mcp/server/shutdown.mjs +40 -0
  87. package/src/mcp/sync/evidence-architecture.mjs +54 -0
  88. package/src/mcp/sync/evidence-common.mjs +88 -0
  89. package/src/mcp/sync/evidence-duplicates.mjs +100 -0
  90. package/src/mcp/sync/evidence-health.mjs +56 -0
  91. package/src/mcp/sync/evidence-packages.mjs +95 -0
  92. package/src/mcp/sync/payload-common.mjs +97 -0
  93. package/src/mcp/sync/payload-v2.mjs +53 -0
  94. package/src/mcp/sync/payload-v3.mjs +79 -0
  95. package/src/mcp/sync-evidence.mjs +7 -402
  96. package/src/mcp/sync-payload.mjs +10 -244
  97. package/src/mcp/tools-actions.mjs +18 -414
  98. package/src/mcp/tools-architecture.mjs +18 -70
  99. package/src/mcp/tools-context.mjs +56 -15
  100. package/src/mcp/tools-endpoints.mjs +4 -1
  101. package/src/mcp/tools-graph.mjs +17 -331
  102. package/src/mcp/tools-health.mjs +24 -705
  103. package/src/mcp-server.mjs +35 -146
  104. package/src/precision/lsp-client/constants.js +12 -0
  105. package/src/precision/lsp-client/environment.js +20 -0
  106. package/src/precision/lsp-client/errors.js +15 -0
  107. package/src/precision/lsp-client/lifecycle.js +127 -0
  108. package/src/precision/lsp-client/message-parser.js +81 -0
  109. package/src/precision/lsp-client/protocol.js +212 -0
  110. package/src/precision/lsp-client/registry.js +58 -0
  111. package/src/precision/lsp-client/repo-uri.js +60 -0
  112. package/src/precision/lsp-client/stdio-client.js +151 -0
  113. package/src/precision/lsp-client.js +10 -682
  114. package/src/precision/lsp-overlay/build.js +238 -0
  115. package/src/precision/lsp-overlay/contract.js +61 -0
  116. package/src/precision/lsp-overlay/reference-results.js +108 -0
  117. package/src/precision/lsp-overlay/semantic-inputs.js +62 -0
  118. package/src/precision/lsp-overlay/source-session.js +134 -0
  119. package/src/precision/lsp-overlay/store.js +150 -0
  120. package/src/precision/lsp-overlay/target-index.js +147 -0
  121. package/src/precision/lsp-overlay/target-query.js +55 -0
  122. package/src/precision/lsp-overlay.js +11 -868
  123. package/src/precision/typescript-lsp-provider.js +12 -682
  124. package/src/precision/typescript-provider/client.js +69 -0
  125. package/src/precision/typescript-provider/discovery.js +124 -0
  126. package/src/precision/typescript-provider/project-host.js +249 -0
  127. package/src/precision/typescript-provider/project-safety.js +161 -0
  128. package/src/precision/typescript-provider/reference-usage.js +53 -0
  129. package/src/version.js +7 -0
  130. package/docs/releases/v0.2.13.md +0 -48
@@ -0,0 +1,212 @@
1
+ import {JSON_RPC_VERSION, positiveInteger} from './constants.js'
2
+ import {LspProtocolError, LspTimeoutError} from './errors.js'
3
+ import {WEAVATRIX_VERSION} from '../../version.js'
4
+
5
+ export function assertPosition(position) {
6
+ if (position == null || typeof position !== 'object') throw new TypeError('position is required')
7
+ const line = Number(position.line)
8
+ const character = Number(position.character)
9
+ if (!Number.isSafeInteger(line) || line < 0 || !Number.isSafeInteger(character) || character < 0) {
10
+ throw new TypeError('position line and character must be non-negative integers')
11
+ }
12
+ return {line, character}
13
+ }
14
+
15
+ function normalizeRange(range) {
16
+ if (range == null || typeof range !== 'object') throw new LspProtocolError('LSP location is missing a range')
17
+ return {start: assertPosition(range.start), end: assertPosition(range.end)}
18
+ }
19
+
20
+ function jsonRpcError(error) {
21
+ const wrapped = new Error(error?.message || 'LSP request failed')
22
+ wrapped.name = 'LspResponseError'
23
+ wrapped.code = error?.code
24
+ wrapped.data = error?.data
25
+ return wrapped
26
+ }
27
+
28
+ export function assertClientWritable(client) {
29
+ if (!['running', 'initialized', 'stopping'].includes(client.state) || client.child.stdin?.destroyed) {
30
+ throw new Error(`LSP client is not writable (state=${client.state})`)
31
+ }
32
+ }
33
+
34
+ export function writeClientMessage(client, message) {
35
+ assertClientWritable(client)
36
+ const body = Buffer.from(JSON.stringify(message), 'utf8')
37
+ if (body.length > client.maxMessageBytes) {
38
+ throw new LspProtocolError(`Outgoing LSP message exceeds ${client.maxMessageBytes} byte limit`)
39
+ }
40
+ const header = Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, 'ascii')
41
+ return new Promise((resolveWrite, rejectWrite) => {
42
+ client.child.stdin.write(
43
+ Buffer.concat([header, body]),
44
+ (error) => error ? rejectWrite(error) : resolveWrite(),
45
+ )
46
+ })
47
+ }
48
+
49
+ export async function requestFromClient(client, method, params = null, {timeoutMs = client.requestTimeoutMs} = {}) {
50
+ if (typeof method !== 'string' || method === '') throw new TypeError('LSP method is required')
51
+ const boundedTimeout = positiveInteger(timeoutMs, client.requestTimeoutMs, 'timeoutMs')
52
+ const id = client.nextId++
53
+ let entry
54
+ const response = new Promise((resolveResponse, rejectResponse) => {
55
+ const timer = setTimeout(() => {
56
+ if (client.pending.get(id) !== entry) return
57
+ client.pending.delete(id)
58
+ void writeClientMessage(client, {
59
+ jsonrpc: JSON_RPC_VERSION,
60
+ method: '$/cancelRequest',
61
+ params: {id},
62
+ }).catch(() => {})
63
+ rejectResponse(new LspTimeoutError(method, boundedTimeout))
64
+ }, boundedTimeout)
65
+ entry = {method, timer, resolve: resolveResponse, reject: rejectResponse}
66
+ client.pending.set(id, entry)
67
+ })
68
+ try {
69
+ await writeClientMessage(client, {jsonrpc: JSON_RPC_VERSION, id, method, params})
70
+ } catch (error) {
71
+ if (client.pending.get(id) === entry) {
72
+ clearTimeout(entry.timer)
73
+ client.pending.delete(id)
74
+ entry.reject(error)
75
+ }
76
+ }
77
+ return response
78
+ }
79
+
80
+ export async function notifyClient(client, method, params = null) {
81
+ if (typeof method !== 'string' || method === '') throw new TypeError('LSP method is required')
82
+ await writeClientMessage(client, {jsonrpc: JSON_RPC_VERSION, method, params})
83
+ }
84
+
85
+ export async function initializeClient(client, {
86
+ capabilities = {},
87
+ initializationOptions,
88
+ clientInfo = {name: 'weavatrix', version: WEAVATRIX_VERSION},
89
+ } = {}) {
90
+ if (client.state !== 'running') throw new Error(`LSP initialize is invalid in state=${client.state}`)
91
+ const workspaceName = client.normalizer.rootPath.split(/[\\/]/).pop() || 'repository'
92
+ const result = await requestFromClient(client, 'initialize', {
93
+ processId: process.pid,
94
+ clientInfo,
95
+ rootUri: client.normalizer.rootUri,
96
+ capabilities,
97
+ initializationOptions,
98
+ workspaceFolders: [{uri: client.normalizer.rootUri, name: workspaceName}],
99
+ })
100
+ await notifyClient(client, 'initialized', {})
101
+ client.state = 'initialized'
102
+ return result
103
+ }
104
+
105
+ export async function openClientDocument(client, {filePath, text, languageId, version = 1}) {
106
+ if (client.state !== 'initialized') throw new Error('LSP client is not initialized')
107
+ if (typeof text !== 'string') throw new TypeError('document text must be a string')
108
+ if (typeof languageId !== 'string' || languageId === '') throw new TypeError('languageId is required')
109
+ if (!Number.isSafeInteger(version) || version < 0) {
110
+ throw new TypeError('document version must be a non-negative integer')
111
+ }
112
+ const normalized = client.normalizer.toUri(filePath)
113
+ await notifyClient(client, 'textDocument/didOpen', {
114
+ textDocument: {uri: normalized.uri, languageId, version, text},
115
+ })
116
+ client.openDocuments.set(normalized.uri, {file: normalized.file, version, languageId})
117
+ return {file: normalized.file, version, languageId}
118
+ }
119
+
120
+ export async function closeClientDocument(client, filePath) {
121
+ const normalized = client.normalizer.toUri(filePath)
122
+ if (!client.openDocuments.has(normalized.uri)) return false
123
+ await notifyClient(client, 'textDocument/didClose', {textDocument: {uri: normalized.uri}})
124
+ client.openDocuments.delete(normalized.uri)
125
+ return true
126
+ }
127
+
128
+ export function normalizeClientLocations(client, result) {
129
+ const locations = []
130
+ for (const raw of result == null ? [] : Array.isArray(result) ? result : [result]) {
131
+ if (raw == null || typeof raw !== 'object') continue
132
+ const uri = raw.uri || raw.targetUri
133
+ const range = raw.range || raw.targetSelectionRange || raw.targetRange
134
+ try {
135
+ const normalized = client.normalizer.fromUri(uri)
136
+ locations.push({file: normalized.file, range: normalizeRange(range)})
137
+ } catch (error) {
138
+ if (error instanceof RangeError) continue
139
+ throw error
140
+ }
141
+ }
142
+ return locations
143
+ }
144
+
145
+ export async function queryClientLocations(client, kind, {filePath, position, includeDeclaration = true, timeoutMs}) {
146
+ const normalized = client.normalizer.toUri(filePath)
147
+ const method = kind === 'references' ? 'textDocument/references' : 'textDocument/definition'
148
+ const params = {textDocument: {uri: normalized.uri}, position: assertPosition(position)}
149
+ if (kind === 'references') params.context = {includeDeclaration: Boolean(includeDeclaration)}
150
+ const result = await requestFromClient(client, method, params, {
151
+ timeoutMs: timeoutMs ?? client.requestTimeoutMs,
152
+ })
153
+ return normalizeClientLocations(client, result)
154
+ }
155
+
156
+ export async function defaultClientServerRequest(client, method, params) {
157
+ if (method === 'workspace/configuration') return Array.isArray(params?.items) ? params.items.map(() => null) : []
158
+ if (method === 'workspace/workspaceFolders') {
159
+ return [{uri: client.normalizer.rootUri, name: client.normalizer.rootPath.split(/[\\/]/).pop() || 'repository'}]
160
+ }
161
+ if (['client/registerCapability', 'client/unregisterCapability', 'window/workDoneProgress/create'].includes(method)) return null
162
+ if (method === 'workspace/applyEdit') {
163
+ return {applied: false, failureReason: 'Weavatrix precision provider is read-only'}
164
+ }
165
+ const error = new Error(`Unsupported LSP server request: ${method}`)
166
+ error.code = -32601
167
+ throw error
168
+ }
169
+
170
+ export async function handleClientServerRequest(client, message) {
171
+ try {
172
+ const result = client.onServerRequest
173
+ ? await client.onServerRequest(message.method, message.params, client)
174
+ : await defaultClientServerRequest(client, message.method, message.params)
175
+ await writeClientMessage(client, {jsonrpc: JSON_RPC_VERSION, id: message.id, result: result ?? null})
176
+ } catch (error) {
177
+ try {
178
+ await writeClientMessage(client, {
179
+ jsonrpc: JSON_RPC_VERSION,
180
+ id: message.id,
181
+ error: {
182
+ code: Number.isInteger(error?.code) ? error.code : -32603,
183
+ message: error?.message || 'LSP client error',
184
+ },
185
+ })
186
+ } catch { /* process exited while request handler was running */ }
187
+ }
188
+ }
189
+
190
+ export function handleClientMessage(client, message) {
191
+ if (message.jsonrpc !== JSON_RPC_VERSION) throw new LspProtocolError('Unsupported JSON-RPC version from LSP server')
192
+ if (typeof message.method === 'string') {
193
+ if (message.id != null) void handleClientServerRequest(client, message)
194
+ else client.onNotification?.(message.method, message.params, client)
195
+ return
196
+ }
197
+ if (message.id == null) throw new LspProtocolError('LSP response is missing an id')
198
+ const entry = client.pending.get(message.id)
199
+ if (!entry) return
200
+ client.pending.delete(message.id)
201
+ clearTimeout(entry.timer)
202
+ if (message.error != null) entry.reject(jsonRpcError(message.error))
203
+ else entry.resolve(message.result)
204
+ }
205
+
206
+ export function rejectPendingRequests(client, error) {
207
+ for (const entry of client.pending.values()) {
208
+ clearTimeout(entry.timer)
209
+ entry.reject(error)
210
+ }
211
+ client.pending.clear()
212
+ }
@@ -0,0 +1,58 @@
1
+ const ACTIVE_LSP_CLIENTS = new Set()
2
+ let acceptingLspClients = true
3
+
4
+ export function assertLspClientCreationAllowed() {
5
+ if (!acceptingLspClients) throw new Error('LSP process creation is disabled during MCP shutdown')
6
+ }
7
+
8
+ export function registerLspClient(client) {
9
+ ACTIVE_LSP_CLIENTS.add(client)
10
+ }
11
+
12
+ export function unregisterLspClient(client) {
13
+ ACTIVE_LSP_CLIENTS.delete(client)
14
+ }
15
+
16
+ export function isLspClientActive(client) {
17
+ return ACTIVE_LSP_CLIENTS.has(client)
18
+ }
19
+
20
+ export function activeLspClientCount() {
21
+ return ACTIVE_LSP_CLIENTS.size
22
+ }
23
+
24
+ export function beginLspClientShutdown() {
25
+ acceptingLspClients = false
26
+ }
27
+
28
+ export async function shutdownActiveLspClients({timeoutMs = 3_000} = {}) {
29
+ const boundedTimeout = Math.max(250, Math.min(10_000, Number(timeoutMs) || 3_000))
30
+ const startedAt = Date.now()
31
+ const clients = [...ACTIVE_LSP_CLIENTS]
32
+ if (!clients.length) return {requested: 0, remaining: 0, timedOut: false}
33
+ let timer
34
+ const gracefulBudget = Math.max(125, Math.floor(boundedTimeout * 0.6))
35
+ const graceful = Promise.allSettled(clients.map((client) => client.shutdown({timeoutMs: boundedTimeout})))
36
+ const outcome = await Promise.race([
37
+ graceful.then(() => 'closed'),
38
+ new Promise((resolveOutcome) => { timer = setTimeout(() => resolveOutcome('timeout'), gracefulBudget) }),
39
+ ])
40
+ if (timer) clearTimeout(timer)
41
+ const survivors = [...ACTIVE_LSP_CLIENTS]
42
+ if (survivors.length) {
43
+ const reason = new Error(`MCP shutdown timed out with ${ACTIVE_LSP_CLIENTS.size} active LSP client(s)`)
44
+ const forceBudget = Math.max(100, boundedTimeout - (Date.now() - startedAt))
45
+ await Promise.allSettled(survivors.map(async (client) => {
46
+ if (typeof client.killWindowsTreeAndWait === 'function') await client.killWindowsTreeAndWait(forceBudget)
47
+ client.kill(reason)
48
+ if (typeof client.waitForExit === 'function') {
49
+ await client.waitForExit(Math.max(100, boundedTimeout - (Date.now() - startedAt)))
50
+ }
51
+ }))
52
+ }
53
+ return {
54
+ requested: clients.length,
55
+ remaining: ACTIVE_LSP_CLIENTS.size,
56
+ timedOut: outcome === 'timeout' || ACTIVE_LSP_CLIENTS.size > 0,
57
+ }
58
+ }
@@ -0,0 +1,60 @@
1
+ import {realpathSync} from 'node:fs'
2
+ import {isAbsolute, relative, resolve, sep} from 'node:path'
3
+ import {fileURLToPath, pathToFileURL} from 'node:url'
4
+
5
+ function normalizeFilesystemPath(path) {
6
+ let normalized = path
7
+ if (process.platform === 'win32' && normalized.startsWith('\\\\?\\UNC\\')) normalized = `\\\\${normalized.slice(8)}`
8
+ else if (process.platform === 'win32' && normalized.startsWith('\\\\?\\')) normalized = normalized.slice(4)
9
+ return resolve(normalized)
10
+ }
11
+
12
+ function realpathIfPossible(path) {
13
+ try { return normalizeFilesystemPath(realpathSync.native(path)) }
14
+ catch { return normalizeFilesystemPath(path) }
15
+ }
16
+
17
+ function existingRealpath(path) {
18
+ try { return normalizeFilesystemPath(realpathSync.native(path)) }
19
+ catch { return null }
20
+ }
21
+
22
+ function pathInside(rootPath, candidatePath) {
23
+ const rel = relative(rootPath, candidatePath)
24
+ return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel))
25
+ }
26
+
27
+ function portableRelative(rootPath, candidatePath) {
28
+ return relative(rootPath, candidatePath).split(sep).join('/')
29
+ }
30
+
31
+ /** Repository-bounded path/URI conversion surface used by the client. */
32
+ export function createRepoUriNormalizer(repoRoot) {
33
+ if (typeof repoRoot !== 'string' || repoRoot.trim() === '') throw new TypeError('repoRoot is required')
34
+ const lexicalRoot = normalizeFilesystemPath(resolve(repoRoot))
35
+ const absoluteRoot = realpathIfPossible(lexicalRoot)
36
+ const toAbsolute = (filePath) => {
37
+ if (typeof filePath !== 'string' || filePath.trim() === '') throw new TypeError('filePath is required')
38
+ const lexicalPath = normalizeFilesystemPath(resolve(lexicalRoot, filePath))
39
+ const canonicalPath = existingRealpath(lexicalPath)
40
+ if (canonicalPath == null) {
41
+ if (!pathInside(lexicalRoot, lexicalPath)) throw new RangeError('LSP path is outside the repository')
42
+ return lexicalPath
43
+ }
44
+ if (!pathInside(absoluteRoot, canonicalPath)) throw new RangeError('LSP path resolves outside the repository')
45
+ return canonicalPath
46
+ }
47
+ const fromUri = (uri) => {
48
+ if (typeof uri !== 'string' || !uri.startsWith('file:')) throw new RangeError('Only file: LSP URIs are accepted')
49
+ let filePath
50
+ try { filePath = fileURLToPath(uri) }
51
+ catch (error) { throw new RangeError('Invalid file: LSP URI', {cause: error}) }
52
+ const absolutePath = toAbsolute(filePath)
53
+ return {file: portableRelative(absoluteRoot, absolutePath), absolutePath, uri: pathToFileURL(absolutePath).href}
54
+ }
55
+ const toUri = (filePath) => {
56
+ const absolutePath = toAbsolute(filePath)
57
+ return {file: portableRelative(absoluteRoot, absolutePath), absolutePath, uri: pathToFileURL(absolutePath).href}
58
+ }
59
+ return {rootPath: absoluteRoot, rootUri: pathToFileURL(absoluteRoot).href, toAbsolute, toUri, fromUri}
60
+ }
@@ -0,0 +1,151 @@
1
+ import {spawn as spawnChild} from 'node:child_process'
2
+ import {isAbsolute} from 'node:path'
3
+ import {
4
+ DEFAULT_MAX_HEADER_BYTES,
5
+ DEFAULT_MAX_MESSAGE_BYTES,
6
+ DEFAULT_REQUEST_TIMEOUT_MS,
7
+ positiveInteger,
8
+ } from './constants.js'
9
+ import {lspChildProcessEnv} from './environment.js'
10
+ import {ContentLengthMessageParser} from './message-parser.js'
11
+ import {createRepoUriNormalizer} from './repo-uri.js'
12
+ import {
13
+ assertLspClientCreationAllowed,
14
+ registerLspClient,
15
+ unregisterLspClient,
16
+ } from './registry.js'
17
+ import {
18
+ assertClientWritable,
19
+ closeClientDocument,
20
+ defaultClientServerRequest,
21
+ handleClientServerRequest,
22
+ handleClientMessage,
23
+ initializeClient,
24
+ normalizeClientLocations,
25
+ notifyClient,
26
+ openClientDocument,
27
+ queryClientLocations,
28
+ rejectPendingRequests,
29
+ requestFromClient,
30
+ writeClientMessage,
31
+ } from './protocol.js'
32
+ import {
33
+ failClient,
34
+ killClient,
35
+ killWindowsTreeAndWait,
36
+ shutdownClient,
37
+ shutdownClientOnce,
38
+ waitForClientExit,
39
+ } from './lifecycle.js'
40
+
41
+ export class StdioLspClient {
42
+ constructor({
43
+ repoRoot,
44
+ executablePath = process.execPath,
45
+ args = [],
46
+ env = {},
47
+ requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
48
+ maxMessageBytes = DEFAULT_MAX_MESSAGE_BYTES,
49
+ maxHeaderBytes = DEFAULT_MAX_HEADER_BYTES,
50
+ onNotification,
51
+ onServerRequest,
52
+ spawn = spawnChild,
53
+ } = {}) {
54
+ if (typeof executablePath !== 'string' || !isAbsolute(executablePath)) {
55
+ throw new TypeError('LSP executablePath must be absolute')
56
+ }
57
+ if (!Array.isArray(args) || args.some((arg) => typeof arg !== 'string')) {
58
+ throw new TypeError('LSP args must be strings')
59
+ }
60
+ if (typeof spawn !== 'function') throw new TypeError('spawn must be a function')
61
+ assertLspClientCreationAllowed()
62
+ this.normalizer = createRepoUriNormalizer(repoRoot)
63
+ this.requestTimeoutMs = positiveInteger(requestTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS, 'requestTimeoutMs')
64
+ this.maxMessageBytes = positiveInteger(maxMessageBytes, DEFAULT_MAX_MESSAGE_BYTES, 'maxMessageBytes')
65
+ this.onNotification = typeof onNotification === 'function' ? onNotification : null
66
+ this.onServerRequest = typeof onServerRequest === 'function' ? onServerRequest : null
67
+ this.pending = new Map()
68
+ this.openDocuments = new Map()
69
+ this.nextId = 1
70
+ this.state = 'starting'
71
+ this.stderrTail = ''
72
+ this.shutdownPromise = null
73
+ this.child = spawn(executablePath, args, {
74
+ cwd: this.normalizer.rootPath,
75
+ env: lspChildProcessEnv(env),
76
+ shell: false,
77
+ detached: process.platform !== 'win32',
78
+ windowsHide: true,
79
+ stdio: ['pipe', 'pipe', 'pipe'],
80
+ })
81
+ this.processGroupPid = process.platform !== 'win32' && Number.isInteger(this.child.pid)
82
+ ? this.child.pid : null
83
+ registerLspClient(this)
84
+ this.parser = new ContentLengthMessageParser({
85
+ maxMessageBytes: this.maxMessageBytes,
86
+ maxHeaderBytes,
87
+ onMessage: (message) => this.handleMessage(message),
88
+ })
89
+ this.child.stdout?.on('data', (chunk) => {
90
+ try { this.parser.push(chunk) }
91
+ catch (error) { this.fail(error) }
92
+ })
93
+ this.child.stderr?.setEncoding('utf8')
94
+ this.child.stderr?.on('data', (chunk) => {
95
+ this.stderrTail = `${this.stderrTail}${chunk}`.slice(-16_000)
96
+ })
97
+ this.spawned = new Promise((resolveSpawn, rejectSpawn) => {
98
+ this.child.once('spawn', () => {
99
+ if (this.state === 'starting') this.state = 'running'
100
+ resolveSpawn(this)
101
+ })
102
+ this.child.once('error', (error) => {
103
+ this.fail(error)
104
+ rejectSpawn(error)
105
+ })
106
+ })
107
+ this.exited = new Promise((resolveExit) => {
108
+ let settled = false
109
+ const finishExit = (code, signal) => {
110
+ if (settled) return
111
+ settled = true
112
+ const wasStopping = this.state === 'stopping' || this.state === 'closed'
113
+ this.state = 'closed'
114
+ const suffix = this.stderrTail ? `\n${this.stderrTail}` : ''
115
+ this.rejectPending(new Error(`LSP server exited (code=${code}, signal=${signal})${suffix}`))
116
+ if (!wasStopping && code !== 0) this.openDocuments.clear()
117
+ unregisterLspClient(this)
118
+ resolveExit({code, signal})
119
+ }
120
+ this.child.once('exit', finishExit)
121
+ this.child.once('close', finishExit)
122
+ })
123
+ }
124
+
125
+ async start() { await this.spawned; return this }
126
+ assertWritable() { return assertClientWritable(this) }
127
+ writeMessage(message) { return writeClientMessage(this, message) }
128
+ request(method, params, options) { return requestFromClient(this, method, params, options) }
129
+ notify(method, params) { return notifyClient(this, method, params) }
130
+ initialize(options) { return initializeClient(this, options) }
131
+ openDocument(options) { return openClientDocument(this, options) }
132
+ closeDocument(filePath) { return closeClientDocument(this, filePath) }
133
+ normalizeLocations(result) { return normalizeClientLocations(this, result) }
134
+ definition(options) { return queryClientLocations(this, 'definition', options) }
135
+ references(options) { return queryClientLocations(this, 'references', options) }
136
+ defaultServerRequest(method, params) { return defaultClientServerRequest(this, method, params) }
137
+ handleServerRequest(message) { return handleClientServerRequest(this, message) }
138
+ handleMessage(message) { return handleClientMessage(this, message) }
139
+ rejectPending(error) { return rejectPendingRequests(this, error) }
140
+ fail(error) { return failClient(this, error) }
141
+ kill(reason) { return killClient(this, reason) }
142
+ killWindowsTreeAndWait(timeoutMs) { return killWindowsTreeAndWait(this, timeoutMs) }
143
+ waitForExit(timeoutMs) { return waitForClientExit(this, timeoutMs) }
144
+ shutdown(options) { return shutdownClient(this, options) }
145
+ shutdownOnce(options) { return shutdownClientOnce(this, options) }
146
+ }
147
+
148
+ export async function startStdioLspClient(options) {
149
+ const client = new StdioLspClient(options)
150
+ return client.start()
151
+ }