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
|
@@ -5,16 +5,11 @@ import {childProcessEnv} from '../child-env.js'
|
|
|
5
5
|
import {classifyChangeImpact} from '../analysis/change-classification.js'
|
|
6
6
|
import {readCoverageForRepo} from '../analysis/coverage-reports.js'
|
|
7
7
|
import {computeStaticTestReachability} from '../analysis/static-test-reachability.js'
|
|
8
|
-
import {isStructuralRelation} from '../graph/relations.js'
|
|
9
8
|
import {degreeOf, labelOf, rawGraph} from './graph-context.mjs'
|
|
9
|
+
import {reverseReach} from './graph/reverse-reach.mjs'
|
|
10
|
+
import {gitLines} from './git-output.mjs'
|
|
10
11
|
import {toolResult} from './tool-result.mjs'
|
|
11
12
|
|
|
12
|
-
function gitLines(repoRoot, args) {
|
|
13
|
-
const result = spawnSync('git', ['-C', repoRoot, ...args], {encoding: 'utf8', timeout: 8000, env: childProcessEnv()})
|
|
14
|
-
if (result.status !== 0) return null
|
|
15
|
-
return String(result.stdout || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
13
|
function gitValue(repoRoot, args) {
|
|
19
14
|
const result = spawnSync('git', ['-C', repoRoot, ...args], {encoding: 'utf8', timeout: 8000, env: childProcessEnv()})
|
|
20
15
|
if (result.status !== 0) return null
|
|
@@ -30,37 +25,6 @@ function resolveImpactBase(repoRoot, requested) {
|
|
|
30
25
|
return null
|
|
31
26
|
}
|
|
32
27
|
|
|
33
|
-
function reverseReach(g, seeds, maxDepth) {
|
|
34
|
-
const states = new Map([...seeds].map((id) => [String(id), {
|
|
35
|
-
runtimeDepth: 0, runtimeRelation: null, compileDepth: null, compileRelation: null,
|
|
36
|
-
}]))
|
|
37
|
-
const frontier = [...seeds].map((id) => ({id: String(id), depth: 0, compileOnly: false}))
|
|
38
|
-
for (let cursor = 0; cursor < frontier.length; cursor++) {
|
|
39
|
-
const current = frontier[cursor]
|
|
40
|
-
if (current.depth >= maxDepth) continue
|
|
41
|
-
for (const edge of g.inn.get(current.id) || []) {
|
|
42
|
-
if (isStructuralRelation(edge.relation) || edge.barrelProxy === true) continue
|
|
43
|
-
const id = String(edge.id)
|
|
44
|
-
const compileOnly = current.compileOnly || edge.typeOnly === true || edge.compileOnly === true
|
|
45
|
-
const depth = current.depth + 1
|
|
46
|
-
const entry = states.get(id) || {runtimeDepth: null, runtimeRelation: null, compileDepth: null, compileRelation: null}
|
|
47
|
-
const depthKey = compileOnly ? 'compileDepth' : 'runtimeDepth'
|
|
48
|
-
const relationKey = compileOnly ? 'compileRelation' : 'runtimeRelation'
|
|
49
|
-
if (entry[depthKey] != null && entry[depthKey] <= depth) continue
|
|
50
|
-
entry[depthKey] = depth
|
|
51
|
-
entry[relationKey] = edge.relation || 'rel'
|
|
52
|
-
states.set(id, entry)
|
|
53
|
-
frontier.push({id, depth, compileOnly})
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return new Map([...states].map(([id, entry]) => [id, {
|
|
57
|
-
...entry,
|
|
58
|
-
depth: entry.runtimeDepth ?? entry.compileDepth ?? 0,
|
|
59
|
-
compileOnly: entry.runtimeDepth == null,
|
|
60
|
-
relation: entry.runtimeDepth != null ? entry.runtimeRelation : entry.compileRelation,
|
|
61
|
-
}]))
|
|
62
|
-
}
|
|
63
|
-
|
|
64
28
|
const impactKind = (entry) => entry?.runtimeDepth != null
|
|
65
29
|
? (entry.compileDepth != null ? `runtime + compile-time(d${entry.compileDepth})` : 'runtime')
|
|
66
30
|
: 'compile-time'
|
package/src/mcp/tools-impact.mjs
CHANGED
|
@@ -9,46 +9,11 @@ import {
|
|
|
9
9
|
rawGraph, prevGraphPathFor, edgeEndpoint, diffGraphs, formatGraphDiff,
|
|
10
10
|
} from './graph-context.mjs'
|
|
11
11
|
import {readCoverageForRepo} from '../analysis/coverage-reports.js'
|
|
12
|
-
import {
|
|
12
|
+
import {reverseReach} from './graph/reverse-reach.mjs'
|
|
13
|
+
import {gitLines} from './git-output.mjs'
|
|
13
14
|
import {tChangeImpactV2} from './tools-impact-change.mjs'
|
|
14
15
|
import {buildGraphAtGitRef} from '../analysis/git-ref-graph.js'
|
|
15
16
|
|
|
16
|
-
function reverseReach(g, seeds, maxDepth) {
|
|
17
|
-
const states = new Map([...seeds].map((id) => [String(id), {
|
|
18
|
-
runtimeDepth: 0, runtimeRelation: null, compileDepth: null, compileRelation: null,
|
|
19
|
-
}]))
|
|
20
|
-
const frontier = [...seeds].map((id) => ({id: String(id), depth: 0, compileOnly: false}))
|
|
21
|
-
for (let cursor = 0; cursor < frontier.length; cursor++) {
|
|
22
|
-
const current = frontier[cursor]
|
|
23
|
-
if (current.depth >= maxDepth) continue
|
|
24
|
-
for (const e of g.inn.get(current.id) || []) {
|
|
25
|
-
if (isStructuralRelation(e.relation) || e.barrelProxy === true) continue
|
|
26
|
-
const id = String(e.id)
|
|
27
|
-
const compileOnly = current.compileOnly || e.typeOnly === true || e.compileOnly === true
|
|
28
|
-
const depth = current.depth + 1
|
|
29
|
-
const entry = states.get(id) || {
|
|
30
|
-
runtimeDepth: null, runtimeRelation: null, compileDepth: null, compileRelation: null,
|
|
31
|
-
}
|
|
32
|
-
const depthKey = compileOnly ? 'compileDepth' : 'runtimeDepth'
|
|
33
|
-
const relationKey = compileOnly ? 'compileRelation' : 'runtimeRelation'
|
|
34
|
-
const provenanceKey = compileOnly ? 'compileProvenance' : 'runtimeProvenance'
|
|
35
|
-
if (entry[depthKey] != null && entry[depthKey] <= depth) continue
|
|
36
|
-
entry[depthKey] = depth
|
|
37
|
-
entry[relationKey] = e.relation || 'rel'
|
|
38
|
-
entry[provenanceKey] = e.provenance || 'UNKNOWN'
|
|
39
|
-
states.set(id, entry)
|
|
40
|
-
frontier.push({id, depth, compileOnly})
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return new Map([...states].map(([id, entry]) => [id, {
|
|
44
|
-
...entry,
|
|
45
|
-
depth: entry.runtimeDepth ?? entry.compileDepth ?? 0,
|
|
46
|
-
compileOnly: entry.runtimeDepth == null,
|
|
47
|
-
relation: entry.runtimeDepth != null ? entry.runtimeRelation : entry.compileRelation,
|
|
48
|
-
provenance: entry.runtimeDepth != null ? entry.runtimeProvenance : entry.compileProvenance,
|
|
49
|
-
}]))
|
|
50
|
-
}
|
|
51
|
-
|
|
52
17
|
const impactKind = (entry) => {
|
|
53
18
|
if (entry?.runtimeDepth != null) {
|
|
54
19
|
return entry.compileDepth != null ? `runtime + compile-time(d${entry.compileDepth})` : 'runtime'
|
|
@@ -141,12 +106,6 @@ export async function tGraphDiff(g, args, ctx) {
|
|
|
141
106
|
}
|
|
142
107
|
|
|
143
108
|
// ---- change impact -------------------------------------------------------------------------------
|
|
144
|
-
function gitLines(repoRoot, args) {
|
|
145
|
-
const res = spawnSync('git', ['-C', repoRoot, ...args], {encoding: 'utf8', timeout: 8000, env: childProcessEnv()})
|
|
146
|
-
if (res.status !== 0) return null
|
|
147
|
-
return String(res.stdout || '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
109
|
function resolveImpactBase(repoRoot, requested) {
|
|
151
110
|
const candidates = requested ? [requested] : ['origin/HEAD', 'origin/main', 'origin/master', 'main', 'master']
|
|
152
111
|
for (const ref of candidates) {
|
package/src/mcp-server.mjs
CHANGED
|
@@ -16,54 +16,24 @@
|
|
|
16
16
|
// go to stderr. Two argv forms:
|
|
17
17
|
// weavatrix-mcp <repoRoot> [caps] — graph path derived from the standard layout
|
|
18
18
|
// weavatrix-mcp <graph.json> <repoRoot> [caps] — explicit graph file (classic form)
|
|
19
|
-
import {existsSync, statSync, realpathSync} from 'node:fs'
|
|
20
|
-
import {join, dirname} from 'node:path'
|
|
21
|
-
import {fileURLToPath} from 'node:url'
|
|
22
19
|
import process from 'node:process'
|
|
23
20
|
import {loadGraph} from './mcp/graph-context.mjs'
|
|
24
|
-
import {graphOutDirForRepo} from './graph/layout.js'
|
|
25
|
-
import {createRequire} from 'node:module'
|
|
26
21
|
import {createStalenessNoticeGate} from './mcp/staleness-notice.mjs'
|
|
27
22
|
import {normalizeToolResult} from './mcp/tool-result.mjs'
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
23
|
+
import {runtimeVersionStatus, staleRuntimeMessage} from './mcp/runtime-version.mjs'
|
|
24
|
+
import {createAutoRefresh} from './mcp/server/auto-refresh.mjs'
|
|
25
|
+
import {createShutdownController} from './mcp/server/shutdown.mjs'
|
|
26
|
+
import {
|
|
27
|
+
hotCatalogVersion as hotVersion, loadServerCatalog as loadCatalog, PACKAGE_JSON_PATH,
|
|
28
|
+
PACKAGE_VERSION as PKG_VERSION, resolveServerTarget, SERVER_INFO,
|
|
29
|
+
} from './mcp/server/runtime-config.mjs'
|
|
32
30
|
|
|
33
31
|
// version comes from package.json so serverInfo can never drift from the published package again
|
|
34
|
-
const PKG_VERSION = (() => { try { return createRequire(import.meta.url)('../package.json').version } catch { return '0.0.0' } })()
|
|
35
|
-
const SERVER_INFO = {name: 'weavatrix', version: PKG_VERSION}
|
|
36
32
|
const DEFAULT_PROTOCOL = '2024-11-05'
|
|
37
33
|
const log = (...a) => process.stderr.write(`[weavatrix] ${a.join(' ')}\n`)
|
|
38
34
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const settled = await Promise.race([
|
|
42
|
-
Promise.resolve(promise).then(() => true, () => true),
|
|
43
|
-
new Promise((resolveSettled) => { timer = setTimeout(() => resolveSettled(false), timeoutMs) }),
|
|
44
|
-
])
|
|
45
|
-
if (timer) clearTimeout(timer)
|
|
46
|
-
return settled
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// argv[2] is a repo DIRECTORY in the npx form — derive the graph location from the standard layout;
|
|
50
|
-
// otherwise it is the graph.json path and the repo root follows it.
|
|
51
|
-
let GRAPH_PATH = process.argv[2]
|
|
52
|
-
let repoArg = process.argv[3]
|
|
53
|
-
// caps ABSENT (undefined) = offline defaults (explicit local retargeting, no network).
|
|
54
|
-
// PRESENT (even the empty string) = explicit set — see catalog.loadHotApi.
|
|
55
|
-
let CAPS_ARG = process.argv[4]
|
|
56
|
-
try {
|
|
57
|
-
if (GRAPH_PATH && statSync(GRAPH_PATH).isDirectory()) {
|
|
58
|
-
repoArg = realpathSync.native(GRAPH_PATH)
|
|
59
|
-
CAPS_ARG = process.argv[3]
|
|
60
|
-
GRAPH_PATH = join(graphOutDirForRepo(repoArg), 'graph.json')
|
|
61
|
-
if (!existsSync(GRAPH_PATH)) log(`no graph built yet for ${repoArg} — ask the agent to call rebuild_graph; it builds into the standard weavatrix-graphs layout`)
|
|
62
|
-
}
|
|
63
|
-
} catch { /* argv[2] is not a directory → classic <graph.json> <repoRoot> form */ }
|
|
64
|
-
// repo source root for search_code / read_source; null → those tools degrade.
|
|
65
|
-
let REPO_ROOT = null
|
|
66
|
-
try { if (repoArg && statSync(repoArg).isDirectory()) REPO_ROOT = realpathSync.native(repoArg) } catch { /* invalid repo root */ }
|
|
35
|
+
const target = resolveServerTarget(process.argv, log)
|
|
36
|
+
const GRAPH_PATH = target.graphPath, REPO_ROOT = target.repoRoot, CAPS_ARG = target.capabilities
|
|
67
37
|
|
|
68
38
|
// ---- hot reload of tool implementations -----------------------------------------------------------
|
|
69
39
|
// Node caches modules at spawn, so edits to the tool code would otherwise be invisible until the MCP
|
|
@@ -71,29 +41,21 @@ try { if (repoArg && statSync(repoArg).isDirectory()) REPO_ROOT = realpathSync.n
|
|
|
71
41
|
// any changed on disk we re-import them through catalog.loadHotApi with a cache-busting version and
|
|
72
42
|
// swap the tool table, then notify the client. The stdio shell, graph-context (its caches), and the
|
|
73
43
|
// analysis engines are NOT swapped — changing those still needs a reconnect.
|
|
74
|
-
const MCP_DIR = join(dirname(fileURLToPath(import.meta.url)), 'mcp')
|
|
75
|
-
const CATALOG_URL = new URL('./mcp/catalog.mjs', import.meta.url)
|
|
76
|
-
const loadCatalog = (version = 0) => import(version ? `${CATALOG_URL.href}?v=${version}` : CATALOG_URL.href)
|
|
77
|
-
function hotVersion(hotFiles) {
|
|
78
|
-
let v = 0
|
|
79
|
-
for (const f of hotFiles) {
|
|
80
|
-
try { const t = statSync(join(MCP_DIR, f)).mtimeMs; if (t > v) v = t } catch { /* missing file just doesn't bump the version */ }
|
|
81
|
-
}
|
|
82
|
-
return v
|
|
83
|
-
}
|
|
84
|
-
|
|
85
44
|
async function main() {
|
|
86
45
|
let catalog = await loadCatalog()
|
|
87
46
|
let api = await catalog.loadHotApi(0, CAPS_ARG)
|
|
88
47
|
const runtimeInfo = () => ({
|
|
89
|
-
|
|
48
|
+
...runtimeVersionStatus({runningVersion: PKG_VERSION, packageJsonPath: PACKAGE_JSON_PATH}),
|
|
90
49
|
profile: api.profile || 'custom',
|
|
91
50
|
capabilities: [...api.caps],
|
|
92
51
|
toolCount: api.tools.length,
|
|
93
52
|
})
|
|
94
53
|
const runtimeInstructions = () => {
|
|
95
54
|
const runtime = runtimeInfo()
|
|
96
|
-
|
|
55
|
+
const stale = runtime.staleRuntime
|
|
56
|
+
? ` ${staleRuntimeMessage(runtime)}${runtime.staleRuntimeAllowed ? ' Development override is active.' : ''}`
|
|
57
|
+
: ''
|
|
58
|
+
return `Weavatrix ${runtime.version}; diskVersion=${runtime.diskVersion || 'unavailable'}; profile=${runtime.profile}; tools=${runtime.toolCount}; capabilities=${runtime.capabilities.join(',') || '(none)'}.${stale} If this differs from the client-visible tool list, reconnect the MCP client to discard its cached schema.`
|
|
97
59
|
}
|
|
98
60
|
let graph = null
|
|
99
61
|
let graphError = null
|
|
@@ -119,73 +81,12 @@ async function main() {
|
|
|
119
81
|
// ordinary tools use a per-call graph/context snapshot, so an explicitly retargetable registration
|
|
120
82
|
// cannot mix one repo's in-memory graph with another repo's source root under concurrent MCP calls.
|
|
121
83
|
let targetMutation = Promise.resolve()
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const refreshProbeCache = new Map()
|
|
125
|
-
const configuredDebounce = process.env.WEAVATRIX_AUTO_REFRESH_DEBOUNCE_MS == null
|
|
126
|
-
? 2_000
|
|
127
|
-
: Number(process.env.WEAVATRIX_AUTO_REFRESH_DEBOUNCE_MS)
|
|
128
|
-
const refreshDebounceMs = Math.max(0, Math.min(5_000, Number.isFinite(configuredDebounce) ? configuredDebounce : 2_000))
|
|
129
|
-
const autoRefresh = async (callCtx, currentGraph) => {
|
|
130
|
-
if (!callCtx?.repoRoot || !callCtx?.graphPath) return {graph: null, refresh: null}
|
|
131
|
-
const activePrecision = currentGraph?.graphPrecisionMode || defaultPrecisionMode()
|
|
132
|
-
const probeKey = `${callCtx.graphPath}\0${currentGraph?.graphBuildMode || 'full'}\0${activePrecision}`
|
|
133
|
-
// Semantic inputs include ignored configs and configured project files that Git status does
|
|
134
|
-
// not see. Check their bounded fingerprint before the ordinary source freshness debounce;
|
|
135
|
-
// exact evidence must have no stale window, even on back-to-back tool calls.
|
|
136
|
-
let semanticInputsChanged = false
|
|
137
|
-
if (activePrecision === 'lsp' && currentGraph) {
|
|
138
|
-
try {
|
|
139
|
-
const overlay = readPrecisionOverlay(callCtx.graphPath, currentGraph)
|
|
140
|
-
semanticInputsChanged = typeof overlay?.semanticInputFingerprint === 'string'
|
|
141
|
-
&& !precisionSemanticInputsMatch(overlay, callCtx.repoRoot, currentGraph)
|
|
142
|
-
} catch {
|
|
143
|
-
semanticInputsChanged = true
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
const cachedProbe = refreshProbeCache.get(probeKey)
|
|
147
|
-
if (!semanticInputsChanged && currentGraph && cachedProbe && Date.now() - cachedProbe.checkedAt < refreshDebounceMs) {
|
|
148
|
-
return {graph: currentGraph, refresh: {kind: 'none', revision: currentGraph.graphRevision || null, changedFiles: 0}}
|
|
149
|
-
}
|
|
150
|
-
const beforeProbe = repositoryFreshnessProbe(callCtx.repoRoot)
|
|
151
|
-
const precisionMissing = activePrecision === 'lsp' && (
|
|
152
|
-
Number(currentGraph?.precisionOverlayV) !== PRECISION_OVERLAY_V
|
|
153
|
-
|| semanticInputsChanged
|
|
154
|
-
)
|
|
155
|
-
if (!precisionMissing && beforeProbe && currentGraph && (
|
|
156
|
-
cachedProbe?.probe === beforeProbe
|
|
157
|
-
|| persistedFreshnessMatches(currentGraph, beforeProbe, currentGraph.graphBuildMode || 'full')
|
|
158
|
-
)) {
|
|
159
|
-
refreshProbeCache.set(probeKey, {probe: beforeProbe, checkedAt: Date.now()})
|
|
160
|
-
return {graph: currentGraph, refresh: {kind: 'none', revision: currentGraph.graphRevision || null, changedFiles: 0}}
|
|
161
|
-
}
|
|
162
|
-
const result = await buildGraphForRepo(callCtx.repoRoot, {
|
|
163
|
-
mode: currentGraph?.graphBuildMode || 'full',
|
|
164
|
-
precision: activePrecision,
|
|
165
|
-
scope: '',
|
|
166
|
-
outDir: dirname(callCtx.graphPath),
|
|
167
|
-
})
|
|
168
|
-
if (!result.ok) throw new Error(result.error || 'automatic graph refresh failed')
|
|
169
|
-
api.resetStalenessCache()
|
|
170
|
-
const fresh = loadGraph(callCtx.graphPath, {repoRoot: callCtx.repoRoot})
|
|
171
|
-
const afterProbe = repositoryFreshnessProbe(callCtx.repoRoot)
|
|
172
|
-
if (afterProbe && afterProbe === beforeProbe) refreshProbeCache.set(probeKey, {probe: afterProbe, checkedAt: Date.now()})
|
|
173
|
-
else refreshProbeCache.delete(probeKey)
|
|
174
|
-
const update = result.refresh || {kind: 'full', changedFiles: [], reason: 'automatic-refresh'}
|
|
175
|
-
return {
|
|
176
|
-
graph: fresh,
|
|
177
|
-
refresh: {
|
|
178
|
-
kind: update.kind,
|
|
179
|
-
revision: update.revision || fresh.graphRevision || null,
|
|
180
|
-
changedFiles: Array.isArray(update.changedFiles) ? update.changedFiles.length : 0,
|
|
181
|
-
notice: update.kind === 'none' ? undefined : `Graph ${update.kind === 'incremental' ? 'incrementally refreshed' : 'rebuilt'} before this answer (${update.reason || 'repository changed'}).`,
|
|
182
|
-
},
|
|
183
|
-
}
|
|
184
|
-
}
|
|
84
|
+
const shutdown = createShutdownController({log, targetMutation: () => targetMutation})
|
|
85
|
+
const autoRefresh = createAutoRefresh(() => api)
|
|
185
86
|
const send = (msg) => {
|
|
186
87
|
// A request that was already running when the client disconnected may complete during the
|
|
187
88
|
// bounded drain. Do not write a late reply into a closed protocol pipe.
|
|
188
|
-
if (
|
|
89
|
+
if (shutdown.isShuttingDown() || process.stdout.destroyed || !process.stdout.writable) return false
|
|
189
90
|
try {
|
|
190
91
|
return process.stdout.write(JSON.stringify(msg) + '\n')
|
|
191
92
|
} catch (error) {
|
|
@@ -194,32 +95,9 @@ async function main() {
|
|
|
194
95
|
}
|
|
195
96
|
}
|
|
196
97
|
const reply = (id, result) => send({jsonrpc: '2.0', id, result})
|
|
197
|
-
const fail = (id, code, message) => send({jsonrpc: '2.0', id, error: {code, message}})
|
|
98
|
+
const fail = (id, code, message, data) => send({jsonrpc: '2.0', id, error: {code, message, ...(data ? {data} : {})}})
|
|
198
99
|
|
|
199
|
-
const requestShutdown =
|
|
200
|
-
if (shutdownPromise) return shutdownPromise
|
|
201
|
-
shuttingDown = true
|
|
202
|
-
// This is synchronous and deliberately precedes every await: a graph build that reaches its
|
|
203
|
-
// precision phase during the drain must not create a fresh TLS/tsserver process tree.
|
|
204
|
-
beginLspClientShutdown()
|
|
205
|
-
process.stdin.pause()
|
|
206
|
-
const activeAtStart = activeLspClientCount()
|
|
207
|
-
log(`shutdown requested (${reason}); draining graph work and ${activeAtStart} semantic provider(s)`)
|
|
208
|
-
shutdownPromise = (async () => {
|
|
209
|
-
// Give an ordinary auto-refresh a chance to commit and close its provider itself. A wedged
|
|
210
|
-
// parse/query is bounded; active semantic children are then closed or tree-killed, after
|
|
211
|
-
// which the mutation gets a final window to observe that cancellation and release locks.
|
|
212
|
-
const initiallyDrained = await settleWithin(targetMutation, 2_500)
|
|
213
|
-
const semantic = await shutdownActiveLspClients({timeoutMs: 3_000})
|
|
214
|
-
const fullyDrained = initiallyDrained || await settleWithin(targetMutation, 1_500)
|
|
215
|
-
log(`shutdown cleanup: graph=${fullyDrained ? 'drained' : 'bounded-timeout'}, semantic=${semantic.requested} requested/${semantic.remaining} remaining${semantic.timedOut ? ' (forced)' : ''}`)
|
|
216
|
-
})().catch((error) => {
|
|
217
|
-
log(`shutdown cleanup failed: ${error.stack || error.message}`)
|
|
218
|
-
}).finally(() => {
|
|
219
|
-
process.exit(exitCode)
|
|
220
|
-
})
|
|
221
|
-
return shutdownPromise
|
|
222
|
-
}
|
|
100
|
+
const requestShutdown = shutdown.request
|
|
223
101
|
process.stdout.on('error', (error) => {
|
|
224
102
|
if (error?.code === 'EPIPE' || error?.code === 'ERR_STREAM_DESTROYED') {
|
|
225
103
|
void requestShutdown('stdout disconnected')
|
|
@@ -252,19 +130,30 @@ async function main() {
|
|
|
252
130
|
const handle = async (msg) => {
|
|
253
131
|
const {id, method, params} = msg
|
|
254
132
|
const isNotification = id === undefined || id === null
|
|
255
|
-
if (
|
|
133
|
+
if (shutdown.isShuttingDown()) {
|
|
256
134
|
if (!isNotification) fail(id, -32000, 'MCP server is shutting down')
|
|
257
135
|
return
|
|
258
136
|
}
|
|
137
|
+
if (method === 'initialize' || method === 'tools/list' || method === 'tools/call') {
|
|
138
|
+
const runtime = runtimeInfo()
|
|
139
|
+
ctx.runtime = runtime
|
|
140
|
+
if (runtime.staleRuntime && !runtime.staleRuntimeAllowed) {
|
|
141
|
+
if (!isNotification) fail(id, -32001, staleRuntimeMessage(runtime), {'weavatrix/runtime': runtime})
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
}
|
|
259
145
|
if (method === 'initialize') {
|
|
260
146
|
if (params?.protocolVersion) protocolVersion = String(params.protocolVersion)
|
|
261
|
-
return reply(id, {
|
|
147
|
+
return reply(id, {
|
|
148
|
+
protocolVersion, capabilities: {tools: {listChanged: true}}, serverInfo: SERVER_INFO,
|
|
149
|
+
instructions: runtimeInstructions(), _meta: {'weavatrix/runtime': runtimeInfo()},
|
|
150
|
+
})
|
|
262
151
|
}
|
|
263
152
|
if (method === 'notifications/initialized' || method === 'initialized') return
|
|
264
153
|
if (method === 'ping') return reply(id, {})
|
|
265
154
|
if (method === 'tools/list') {
|
|
266
155
|
await maybeHotReload()
|
|
267
|
-
if (
|
|
156
|
+
if (shutdown.isShuttingDown()) return
|
|
268
157
|
return reply(id, {
|
|
269
158
|
tools: api.tools.map(({name, description, inputSchema, outputSchema}) => ({name, description, inputSchema, outputSchema})),
|
|
270
159
|
_meta: {'weavatrix/runtime': runtimeInfo()},
|
|
@@ -274,7 +163,7 @@ async function main() {
|
|
|
274
163
|
await maybeHotReload()
|
|
275
164
|
// EOF can arrive while the hot-reload import is pending. Do not enqueue a graph mutation
|
|
276
165
|
// after requestShutdown captured the mutation chain it is responsible for draining.
|
|
277
|
-
if (
|
|
166
|
+
if (shutdown.isShuttingDown()) return
|
|
278
167
|
const tool = api.byName.get(params?.name)
|
|
279
168
|
if (!tool) return reply(id, {content: [{type: 'text', text: `Unknown tool: ${params?.name}`}], isError: true})
|
|
280
169
|
const refreshesGraph = tool.cap === 'graph' || tool.cap === 'health' || tool.refreshGraph === true
|
|
@@ -363,7 +252,7 @@ async function main() {
|
|
|
363
252
|
let buf = ''
|
|
364
253
|
process.stdin.setEncoding('utf8')
|
|
365
254
|
process.stdin.on('data', (chunk) => {
|
|
366
|
-
if (
|
|
255
|
+
if (shutdown.isShuttingDown()) return
|
|
367
256
|
buf += chunk
|
|
368
257
|
let nl
|
|
369
258
|
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
@@ -5,6 +5,20 @@ import { openSync, closeSync, readSync, readFileSync, statSync } from "node:fs";
|
|
|
5
5
|
import { createRepoBoundary } from "./repo-path.js";
|
|
6
6
|
|
|
7
7
|
export const PATH_CLASS_NAMES = Object.freeze(["test", "e2e", "generated", "mock", "story", "docs", "benchmark", "temp"]);
|
|
8
|
+
export const PATH_CLASS_QUERY_TERMS = Object.freeze({
|
|
9
|
+
test: Object.freeze(["test", "tests", "testing", "unit"]),
|
|
10
|
+
e2e: Object.freeze(["e2e", "playwright", "cypress"]),
|
|
11
|
+
generated: Object.freeze(["generated", "autogenerated", "dist"]),
|
|
12
|
+
mock: Object.freeze(["mock", "mocks", "fixture", "fixtures", "fake"]),
|
|
13
|
+
story: Object.freeze(["story", "stories", "storybook"]),
|
|
14
|
+
docs: Object.freeze(["doc", "docs", "documentation", "readme", "guide"]),
|
|
15
|
+
benchmark: Object.freeze(["benchmark", "benchmarks", "bench"]),
|
|
16
|
+
temp: Object.freeze(["temp", "temporary", "tmp"]),
|
|
17
|
+
});
|
|
18
|
+
export const PATH_CLASS_TASK_QUERY_TERMS = Object.freeze({
|
|
19
|
+
...PATH_CLASS_QUERY_TERMS,
|
|
20
|
+
test: Object.freeze([...PATH_CLASS_QUERY_TERMS.test, "spec", "coverage", "verify"]),
|
|
21
|
+
});
|
|
8
22
|
const PATH_CLASS_SET = new Set(PATH_CLASS_NAMES);
|
|
9
23
|
const MAX_CONFIG_BYTES = 64 * 1024;
|
|
10
24
|
const MAX_RULES_PER_CLASS = 64;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const DEFAULT_MAX_MESSAGE_BYTES = 8 * 1024 * 1024
|
|
2
|
+
export const DEFAULT_MAX_HEADER_BYTES = 16 * 1024
|
|
3
|
+
export const DEFAULT_REQUEST_TIMEOUT_MS = 10_000
|
|
4
|
+
export const JSON_RPC_VERSION = '2.0'
|
|
5
|
+
|
|
6
|
+
export function positiveInteger(value, fallback, label) {
|
|
7
|
+
const candidate = value == null ? fallback : Number(value)
|
|
8
|
+
if (!Number.isSafeInteger(candidate) || candidate <= 0) {
|
|
9
|
+
throw new TypeError(`${label} must be a positive integer`)
|
|
10
|
+
}
|
|
11
|
+
return candidate
|
|
12
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {delimiter, dirname, join} from 'node:path'
|
|
2
|
+
import {childProcessEnv} from '../../child-env.js'
|
|
3
|
+
|
|
4
|
+
const LSP_ENV_ALLOWLIST = new Set([
|
|
5
|
+
'path', 'pathext', 'systemroot', 'windir', 'comspec',
|
|
6
|
+
'temp', 'tmp', 'tmpdir', 'home', 'userprofile', 'localappdata', 'appdata',
|
|
7
|
+
'lang', 'language', 'lc_all', 'lc_ctype',
|
|
8
|
+
])
|
|
9
|
+
|
|
10
|
+
export function lspChildProcessEnv(overrides = {}) {
|
|
11
|
+
const inherited = childProcessEnv(overrides)
|
|
12
|
+
const clean = Object.fromEntries(
|
|
13
|
+
Object.entries(inherited).filter(([key]) => LSP_ENV_ALLOWLIST.has(key.toLowerCase())),
|
|
14
|
+
)
|
|
15
|
+
const safePath = [dirname(process.execPath)]
|
|
16
|
+
const systemRoot = inherited.SystemRoot || inherited.SYSTEMROOT || inherited.WINDIR
|
|
17
|
+
if (process.platform === 'win32' && systemRoot) safePath.push(join(systemRoot, 'System32'))
|
|
18
|
+
clean.PATH = [...new Set(safePath)].join(delimiter)
|
|
19
|
+
return clean
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class LspProtocolError extends Error {
|
|
2
|
+
constructor(message, options) {
|
|
3
|
+
super(message, options)
|
|
4
|
+
this.name = 'LspProtocolError'
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class LspTimeoutError extends Error {
|
|
9
|
+
constructor(method, timeoutMs) {
|
|
10
|
+
super(`LSP request timed out after ${timeoutMs}ms: ${method}`)
|
|
11
|
+
this.name = 'LspTimeoutError'
|
|
12
|
+
this.method = method
|
|
13
|
+
this.timeoutMs = timeoutMs
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {spawn as spawnChild} from 'node:child_process'
|
|
2
|
+
import {positiveInteger} from './constants.js'
|
|
3
|
+
import {lspChildProcessEnv} from './environment.js'
|
|
4
|
+
import {LspTimeoutError} from './errors.js'
|
|
5
|
+
import {isLspClientActive} from './registry.js'
|
|
6
|
+
import {notifyClient, rejectPendingRequests, requestFromClient} from './protocol.js'
|
|
7
|
+
|
|
8
|
+
export function failClient(client, error) {
|
|
9
|
+
if (client.state === 'closed' || client.state === 'failed') return
|
|
10
|
+
client.state = 'failed'
|
|
11
|
+
rejectPendingRequests(client, error)
|
|
12
|
+
killClient(client, error)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function killClient(client, reason = new Error('LSP client was killed')) {
|
|
16
|
+
if (client.state !== 'closed') client.state = 'closed'
|
|
17
|
+
rejectPendingRequests(client, reason)
|
|
18
|
+
client.openDocuments.clear()
|
|
19
|
+
try { client.child.stdin?.destroy() } catch { /* already closed */ }
|
|
20
|
+
if (process.platform !== 'win32' && client.processGroupPid) {
|
|
21
|
+
try {
|
|
22
|
+
process.kill(-client.processGroupPid, 'SIGKILL')
|
|
23
|
+
return
|
|
24
|
+
} catch { /* fall through to direct child */ }
|
|
25
|
+
}
|
|
26
|
+
if (client.child.exitCode != null || client.child.signalCode != null) return
|
|
27
|
+
if (process.platform === 'win32' && client.child.pid) {
|
|
28
|
+
try {
|
|
29
|
+
const killer = spawnChild('taskkill', ['/pid', String(client.child.pid), '/T', '/F'], {
|
|
30
|
+
shell: false,
|
|
31
|
+
windowsHide: true,
|
|
32
|
+
env: lspChildProcessEnv(),
|
|
33
|
+
stdio: 'ignore',
|
|
34
|
+
})
|
|
35
|
+
const fallback = () => {
|
|
36
|
+
try { client.child.kill('SIGKILL') } catch { /* already exited */ }
|
|
37
|
+
}
|
|
38
|
+
killer.once('error', fallback)
|
|
39
|
+
killer.once('exit', (code) => { if (code !== 0) fallback() })
|
|
40
|
+
} catch {
|
|
41
|
+
try { client.child.kill('SIGKILL') } catch { /* already exited */ }
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
try { client.child.kill('SIGKILL') } catch { /* already exited */ }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function killWindowsTreeAndWait(client, timeoutMs = 3_000) {
|
|
49
|
+
if (process.platform !== 'win32' || !client.child.pid || client.child.exitCode != null) return
|
|
50
|
+
await new Promise((resolveKill) => {
|
|
51
|
+
let settled = false
|
|
52
|
+
const done = () => {
|
|
53
|
+
if (settled) return
|
|
54
|
+
settled = true
|
|
55
|
+
clearTimeout(timer)
|
|
56
|
+
resolveKill()
|
|
57
|
+
}
|
|
58
|
+
const timer = setTimeout(() => {
|
|
59
|
+
try { client.child.kill() } catch { /* already exited */ }
|
|
60
|
+
done()
|
|
61
|
+
}, Math.max(250, Math.min(5_000, Number(timeoutMs) || 3_000)))
|
|
62
|
+
try {
|
|
63
|
+
const killer = spawnChild('taskkill', ['/pid', String(client.child.pid), '/T', '/F'], {
|
|
64
|
+
shell: false,
|
|
65
|
+
windowsHide: true,
|
|
66
|
+
env: lspChildProcessEnv(),
|
|
67
|
+
stdio: 'ignore',
|
|
68
|
+
})
|
|
69
|
+
const fallback = () => {
|
|
70
|
+
try { client.child.kill('SIGKILL') } catch { /* already exited */ }
|
|
71
|
+
}
|
|
72
|
+
killer.once('error', () => { fallback(); done() })
|
|
73
|
+
killer.once('exit', (code) => {
|
|
74
|
+
if (code !== 0) fallback()
|
|
75
|
+
done()
|
|
76
|
+
})
|
|
77
|
+
} catch {
|
|
78
|
+
try { client.child.kill('SIGKILL') } catch { /* already exited */ }
|
|
79
|
+
done()
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function waitForClientExit(client, timeoutMs = client.requestTimeoutMs) {
|
|
85
|
+
if (!isLspClientActive(client)) return true
|
|
86
|
+
const boundedTimeout = Math.max(100, Math.min(10_000, Number(timeoutMs) || client.requestTimeoutMs))
|
|
87
|
+
let timer
|
|
88
|
+
const outcome = await Promise.race([
|
|
89
|
+
client.exited.then(() => true),
|
|
90
|
+
new Promise((resolveExit) => { timer = setTimeout(() => resolveExit(false), boundedTimeout) }),
|
|
91
|
+
])
|
|
92
|
+
if (timer) clearTimeout(timer)
|
|
93
|
+
return outcome
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function shutdownClient(client, options = {}) {
|
|
97
|
+
if (!client.shutdownPromise) client.shutdownPromise = shutdownClientOnce(client, options)
|
|
98
|
+
return client.shutdownPromise
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function shutdownClientOnce(client, {timeoutMs = client.requestTimeoutMs} = {}) {
|
|
102
|
+
const boundedTimeout = positiveInteger(timeoutMs, client.requestTimeoutMs, 'timeoutMs')
|
|
103
|
+
try {
|
|
104
|
+
if (client.state === 'starting') await client.spawned
|
|
105
|
+
if (client.state === 'closed') {
|
|
106
|
+
await waitForClientExit(client, boundedTimeout)
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
if (client.state === 'initialized') {
|
|
110
|
+
await requestFromClient(client, 'shutdown', null, {timeoutMs: boundedTimeout})
|
|
111
|
+
}
|
|
112
|
+
client.state = 'stopping'
|
|
113
|
+
await notifyClient(client, 'exit', null)
|
|
114
|
+
await killWindowsTreeAndWait(client, Math.min(3_000, boundedTimeout))
|
|
115
|
+
if (process.platform !== 'win32' && client.processGroupPid) {
|
|
116
|
+
await waitForClientExit(client, Math.min(1_000, boundedTimeout))
|
|
117
|
+
try { process.kill(-client.processGroupPid, 'SIGKILL') } catch { /* group already exited */ }
|
|
118
|
+
}
|
|
119
|
+
if (!await waitForClientExit(client, boundedTimeout)) {
|
|
120
|
+
killClient(client, new LspTimeoutError('exit', boundedTimeout))
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
killClient(client, error)
|
|
124
|
+
await killWindowsTreeAndWait(client, Math.min(2_000, boundedTimeout))
|
|
125
|
+
await waitForClientExit(client, Math.min(2_000, boundedTimeout))
|
|
126
|
+
}
|
|
127
|
+
}
|