@epoch-agent/plugin-lsp 3.1.2 → 4.0.0
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 +3 -2
- package/dist/index.js +43 -36
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -174,10 +174,11 @@ PR-3,**刻意砍掉的**(不是做到一半放弃)—— 砍的判据和
|
|
|
174
174
|
## 开发
|
|
175
175
|
|
|
176
176
|
```bash
|
|
177
|
-
|
|
177
|
+
npx vitest run --project=plugin-lsp
|
|
178
178
|
```
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
**八个**用例文件(2026-11-03 核:上一版写的是「七个」,漏了 `orphan-reap`)。
|
|
181
|
+
两处值得先看:
|
|
181
182
|
|
|
182
183
|
- [fixtures/fake-server.mjs](__tests__/fixtures/fake-server.mjs) 是一个**手写分帧**
|
|
183
184
|
的假 LSP server(故意不用 `vscode-jsonrpc`,那样才是一份独立的参照实现)。
|
package/dist/index.js
CHANGED
|
@@ -34,7 +34,7 @@ var MAX_OPEN_DOCUMENTS = 64;
|
|
|
34
34
|
var POLL_STEP_MS = 25;
|
|
35
35
|
var LspServerDownError = class extends Error {
|
|
36
36
|
constructor(detail) {
|
|
37
|
-
super(`language server
|
|
37
|
+
super(`language server exited: ${detail}`);
|
|
38
38
|
this.detail = detail;
|
|
39
39
|
this.name = "LspServerDownError";
|
|
40
40
|
}
|
|
@@ -137,8 +137,8 @@ var LspClient = class _LspClient {
|
|
|
137
137
|
this.connection.onRequest(RegistrationRequest.type, () => void 0);
|
|
138
138
|
this.connection.onRequest(UnregistrationRequest.type, () => void 0);
|
|
139
139
|
this.connection.onRequest(WorkDoneProgressCreateRequest.type, () => void 0);
|
|
140
|
-
this.connection.onError(() => this.markDead("JSON-RPC
|
|
141
|
-
this.connection.onClose(() => this.markDead("JSON-RPC
|
|
140
|
+
this.connection.onError(() => this.markDead("JSON-RPC connection error"));
|
|
141
|
+
this.connection.onClose(() => this.markDead("JSON-RPC connection closed"));
|
|
142
142
|
}
|
|
143
143
|
appendStderr(chunk) {
|
|
144
144
|
this.stderr = (this.stderr + chunk).slice(-STDERR_KEEP);
|
|
@@ -146,7 +146,7 @@ var LspClient = class _LspClient {
|
|
|
146
146
|
markDead(detail, onCrash) {
|
|
147
147
|
if (this.dead) return;
|
|
148
148
|
const tail = this.stderr.trim();
|
|
149
|
-
this.dead = tail ? `${detail}
|
|
149
|
+
this.dead = tail ? `${detail}; stderr: ${tail.slice(-500)}` : detail;
|
|
150
150
|
if (!this.disposing) onCrash?.(this.dead);
|
|
151
151
|
}
|
|
152
152
|
async initialize(projectRoot) {
|
|
@@ -258,7 +258,7 @@ var LspClient = class _LspClient {
|
|
|
258
258
|
this.connection.dispose();
|
|
259
259
|
} catch {
|
|
260
260
|
}
|
|
261
|
-
this.dead ??= "
|
|
261
|
+
this.dead ??= "disposed";
|
|
262
262
|
await killTrackedProcess(this.pidValue);
|
|
263
263
|
if (tree.length > 0) await killStampedPids(tree, { escalate: true });
|
|
264
264
|
}
|
|
@@ -322,7 +322,7 @@ var TYPESCRIPT = {
|
|
|
322
322
|
pkg: "typescript-language-server",
|
|
323
323
|
bin: "typescript-language-server",
|
|
324
324
|
args: ["--stdio"],
|
|
325
|
-
install: "npm i -D typescript-language-server typescript
|
|
325
|
+
install: "npm i -D typescript-language-server typescript (or install globally with npm i -g)",
|
|
326
326
|
initTimeoutMs: 3e4,
|
|
327
327
|
initializationOptions: (projectRoot) => {
|
|
328
328
|
const path = tsserverPath(projectRoot);
|
|
@@ -394,7 +394,7 @@ async function classify(paths, workDir, outcome) {
|
|
|
394
394
|
const absPath = isAbsolute(raw) ? resolve2(raw) : resolve2(root, raw);
|
|
395
395
|
const rel = relative(root, absPath);
|
|
396
396
|
if (rel !== "" && (rel.startsWith("..") || isAbsolute(rel))) {
|
|
397
|
-
outcome.rejected.push({ path: raw, reason:
|
|
397
|
+
outcome.rejected.push({ path: raw, reason: `outside the workspace ${root}` });
|
|
398
398
|
continue;
|
|
399
399
|
}
|
|
400
400
|
const key = uriKey(`file://${absPath.split(sep).join("/")}`);
|
|
@@ -404,11 +404,11 @@ async function classify(paths, workDir, outcome) {
|
|
|
404
404
|
try {
|
|
405
405
|
const info = await stat(absPath);
|
|
406
406
|
if (!info.isFile()) {
|
|
407
|
-
outcome.rejected.push({ path: display, reason: "
|
|
407
|
+
outcome.rejected.push({ path: display, reason: "not a file" });
|
|
408
408
|
continue;
|
|
409
409
|
}
|
|
410
410
|
} catch {
|
|
411
|
-
outcome.rejected.push({ path: display, reason: "
|
|
411
|
+
outcome.rejected.push({ path: display, reason: "file does not exist" });
|
|
412
412
|
continue;
|
|
413
413
|
}
|
|
414
414
|
const match = serverForExtension(extname(absPath));
|
|
@@ -452,7 +452,7 @@ async function runGroup(spec, files, input, outcome) {
|
|
|
452
452
|
} catch (err) {
|
|
453
453
|
outcome.rejected.push({
|
|
454
454
|
path: file.display,
|
|
455
|
-
reason:
|
|
455
|
+
reason: `cannot read it (${err instanceof Error ? err.message : String(err)})`
|
|
456
456
|
});
|
|
457
457
|
continue;
|
|
458
458
|
}
|
|
@@ -464,7 +464,7 @@ async function runGroup(spec, files, input, outcome) {
|
|
|
464
464
|
try {
|
|
465
465
|
batch = await acquired.client.diagnose(documents, input.wait ?? DEFAULT_WAIT);
|
|
466
466
|
} catch (err) {
|
|
467
|
-
const reason = err instanceof LspServerDownError ? err.message :
|
|
467
|
+
const reason = err instanceof LspServerDownError ? err.message : `diagnostics failed (${String(err)})`;
|
|
468
468
|
outcome.skipped.push(skip(spec, reason, files));
|
|
469
469
|
return;
|
|
470
470
|
}
|
|
@@ -477,7 +477,11 @@ async function runGroup(spec, files, input, outcome) {
|
|
|
477
477
|
if (batch.pending.length > 0) {
|
|
478
478
|
const stuck = batch.pending.map((key) => byKey.get(key)).filter((f) => !!f);
|
|
479
479
|
outcome.skipped.push(
|
|
480
|
-
skip(
|
|
480
|
+
skip(
|
|
481
|
+
spec,
|
|
482
|
+
`the server gave no result within ${(input.wait ?? DEFAULT_WAIT).timeoutMs}ms`,
|
|
483
|
+
stuck
|
|
484
|
+
)
|
|
481
485
|
);
|
|
482
486
|
}
|
|
483
487
|
}
|
|
@@ -491,12 +495,12 @@ async function diagnosePaths(input) {
|
|
|
491
495
|
};
|
|
492
496
|
const paths = input.paths.slice(0, MAX_FILES_PER_CALL);
|
|
493
497
|
for (const extra of input.paths.slice(MAX_FILES_PER_CALL)) {
|
|
494
|
-
outcome.rejected.push({ path: extra, reason:
|
|
498
|
+
outcome.rejected.push({ path: extra, reason: `at most ${MAX_FILES_PER_CALL} files per call` });
|
|
495
499
|
}
|
|
496
500
|
const candidates = await classify(paths, resolve2(input.workDir), outcome);
|
|
497
501
|
for (const group of groupBySpec(candidates).values()) {
|
|
498
502
|
if (input.signal?.aborted) {
|
|
499
|
-
outcome.skipped.push(skip(group.spec, "
|
|
503
|
+
outcome.skipped.push(skip(group.spec, "cancelled", group.files));
|
|
500
504
|
continue;
|
|
501
505
|
}
|
|
502
506
|
await runGroup(group.spec, group.files, input, outcome);
|
|
@@ -533,7 +537,7 @@ function renderCounts(items) {
|
|
|
533
537
|
}
|
|
534
538
|
function renderFiles(files) {
|
|
535
539
|
if (files.length <= MAX_FILE_NAMES) return files.join(", ");
|
|
536
|
-
return `${files.slice(0, MAX_FILE_NAMES).join(", ")} \
|
|
540
|
+
return `${files.slice(0, MAX_FILE_NAMES).join(", ")} \u2026 (${files.length} total)`;
|
|
537
541
|
}
|
|
538
542
|
function pickItems(items) {
|
|
539
543
|
if (items.length <= MAX_ITEMS) return { shown: items, dropped: 0 };
|
|
@@ -551,29 +555,29 @@ function formatOutcome(outcome) {
|
|
|
551
555
|
const { checked, diagnostics, skipped, unsupported, rejected } = outcome;
|
|
552
556
|
if (diagnostics.length === 0) {
|
|
553
557
|
lines.push(
|
|
554
|
-
checked.length > 0 ?
|
|
558
|
+
checked.length > 0 ? `No problems found \xB7 checked ${checked.length} file(s): ${renderFiles(checked)}` : "No files were checked"
|
|
555
559
|
);
|
|
556
560
|
} else {
|
|
557
561
|
const { shown, dropped } = pickItems(diagnostics);
|
|
558
562
|
lines.push(
|
|
559
|
-
`${diagnostics.length}
|
|
563
|
+
`${diagnostics.length} diagnostic(s) (${renderCounts(diagnostics)}) \xB7 checked ${checked.length} file(s)`,
|
|
560
564
|
"",
|
|
561
565
|
...shown.map(renderItem)
|
|
562
566
|
);
|
|
563
567
|
if (dropped > 0) {
|
|
564
|
-
lines.push("",
|
|
568
|
+
lines.push("", `(${dropped} more not listed; errors were kept first)`);
|
|
565
569
|
}
|
|
566
570
|
}
|
|
567
571
|
for (const skip2 of skipped) {
|
|
568
|
-
lines.push("",
|
|
569
|
-
if (skip2.install) lines.push(`
|
|
570
|
-
if (skip2.files.length > 0) lines.push(`
|
|
572
|
+
lines.push("", `Skipped ${skip2.label}: ${skip2.reason}`);
|
|
573
|
+
if (skip2.install) lines.push(` install: ${skip2.install}`);
|
|
574
|
+
if (skip2.files.length > 0) lines.push(` not checked: ${renderFiles(skip2.files)}`);
|
|
571
575
|
}
|
|
572
576
|
if (unsupported.length > 0) {
|
|
573
|
-
lines.push("",
|
|
577
|
+
lines.push("", `No matching language server; not checked: ${renderFiles(unsupported)}`);
|
|
574
578
|
}
|
|
575
579
|
for (const bad of rejected) {
|
|
576
|
-
lines.push("",
|
|
580
|
+
lines.push("", `Cannot check ${bad.path}: ${bad.reason}`);
|
|
577
581
|
}
|
|
578
582
|
return lines.join("\n");
|
|
579
583
|
}
|
|
@@ -664,7 +668,7 @@ function resolveServer(spec, projectRoot, options = {}) {
|
|
|
664
668
|
if (launch) return { available: true, launch };
|
|
665
669
|
return {
|
|
666
670
|
available: false,
|
|
667
|
-
reason:
|
|
671
|
+
reason: `could not find ${spec.bin} (not in the project's node_modules, nor on PATH)`,
|
|
668
672
|
install: spec.install
|
|
669
673
|
};
|
|
670
674
|
}
|
|
@@ -702,13 +706,13 @@ var LspServerPool = class {
|
|
|
702
706
|
return { ok: true, client: existing.client };
|
|
703
707
|
}
|
|
704
708
|
this.entries.delete(key);
|
|
705
|
-
this.noteFailure(key, "language server
|
|
709
|
+
this.noteFailure(key, "language server process has exited");
|
|
706
710
|
}
|
|
707
711
|
const failed = this.failures.get(key);
|
|
708
712
|
if (failed && failed.count >= MAX_FAILURES) {
|
|
709
713
|
return {
|
|
710
714
|
ok: false,
|
|
711
|
-
reason: `${failed.detail}
|
|
715
|
+
reason: `${failed.detail} (failed ${failed.count} times in a row; not restarting in this session)`
|
|
712
716
|
};
|
|
713
717
|
}
|
|
714
718
|
const inflight = this.starting.get(key);
|
|
@@ -732,15 +736,18 @@ var LspServerPool = class {
|
|
|
732
736
|
projectRoot,
|
|
733
737
|
onCrash: (detail) => {
|
|
734
738
|
this.entries.delete(key);
|
|
735
|
-
this.noteFailure(key, `language server
|
|
739
|
+
this.noteFailure(key, `language server crashed (${detail})`);
|
|
736
740
|
}
|
|
737
741
|
});
|
|
738
742
|
if (!client) {
|
|
739
|
-
this.noteFailure(
|
|
743
|
+
this.noteFailure(
|
|
744
|
+
key,
|
|
745
|
+
`language server failed to start or initialize timed out (>${spec.initTimeoutMs}ms)`
|
|
746
|
+
);
|
|
740
747
|
const now = this.failures.get(key);
|
|
741
748
|
return {
|
|
742
749
|
ok: false,
|
|
743
|
-
reason: now && now.count >= MAX_FAILURES ? `${now.detail}
|
|
750
|
+
reason: now && now.count >= MAX_FAILURES ? `${now.detail} (not restarting in this session)` : now?.detail ?? "failed to start"
|
|
744
751
|
};
|
|
745
752
|
}
|
|
746
753
|
this.entries.set(key, { client, lastUsedAt: this.now() });
|
|
@@ -814,18 +821,18 @@ function fail(code, message, suggestion, started) {
|
|
|
814
821
|
var lspPlugin = {
|
|
815
822
|
name: "@epoch-agent/plugin-lsp",
|
|
816
823
|
version: "0.0.0",
|
|
817
|
-
description: "
|
|
824
|
+
description: "Language-server diagnostics \u2014 type and syntax errors from the project's own language server",
|
|
818
825
|
tools: [
|
|
819
826
|
{
|
|
820
827
|
name: "lsp_diagnostics",
|
|
821
|
-
description:
|
|
828
|
+
description: `Check files with the project's own language server and return type errors, syntax errors and unused-variable warnings. Supported extensions: ${supportedExtensions().join(" ")}. Call it once after changing code and before saying the work is finished, listing the source files you created or modified. It is much faster than running tsc through the shell because the language server is already resident. It only does single-file diagnostics, which is not the same as a full build: problems from build scripts, bundler config or cross-package type resolution are invisible to it. When no language server is installed it reports "skipped" and prints the install command instead of failing.`,
|
|
822
829
|
parameters: {
|
|
823
830
|
type: "object",
|
|
824
831
|
properties: {
|
|
825
832
|
paths: {
|
|
826
833
|
type: "array",
|
|
827
834
|
items: { type: "string" },
|
|
828
|
-
description: '
|
|
835
|
+
description: 'File paths to check (relative to the working directory, or absolute), e.g. ["src/a.ts", "src/b.tsx"].'
|
|
829
836
|
}
|
|
830
837
|
},
|
|
831
838
|
required: ["paths"]
|
|
@@ -839,8 +846,8 @@ var lspPlugin = {
|
|
|
839
846
|
if (!paths) {
|
|
840
847
|
return fail(
|
|
841
848
|
ERR_BAD_PATHS,
|
|
842
|
-
"paths
|
|
843
|
-
'
|
|
849
|
+
"paths must be a non-empty array of file paths",
|
|
850
|
+
'List the source files you changed in this turn, e.g. {"paths": ["src/a.ts"]}',
|
|
844
851
|
started
|
|
845
852
|
);
|
|
846
853
|
}
|
|
@@ -855,8 +862,8 @@ var lspPlugin = {
|
|
|
855
862
|
} catch (err) {
|
|
856
863
|
return fail(
|
|
857
864
|
ERR_UNEXPECTED,
|
|
858
|
-
|
|
859
|
-
"
|
|
865
|
+
`diagnostics failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
866
|
+
"You can keep going, or run the project's own type-check command through terminal instead",
|
|
860
867
|
started
|
|
861
868
|
);
|
|
862
869
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@epoch-agent/plugin-lsp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "LSP 诊断插件 — 探测项目自带的 language server,按文件取语义诊断",
|
|
6
6
|
"repository": {
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"vscode-jsonrpc": "^9.0.1",
|
|
26
26
|
"vscode-languageserver-protocol": "^3.17.5",
|
|
27
|
-
"@epoch-agent/infra": "
|
|
28
|
-
"@epoch-agent/protocol": "
|
|
27
|
+
"@epoch-agent/infra": "4.0.0",
|
|
28
|
+
"@epoch-agent/protocol": "4.0.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/node": "^26.1.2",
|