oh-my-opencode 4.18.0 → 4.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/codex-qa/scripts/lsp-e2e.sh +3654 -0
- package/.agents/skills/opencode-qa/scripts/lsp-e2e.sh +3071 -0
- package/.agents/skills/work-with-pr/SKILL.md +16 -37
- package/.agents/skills/work-with-pr-workspace/evals/evals.json +3 -3
- package/.opencode/skills/work-with-pr/SKILL.md +16 -37
- package/.opencode/skills/work-with-pr-workspace/evals/evals.json +3 -3
- package/dist/cli/index.js +457 -154
- package/dist/cli-node/index.js +457 -154
- package/dist/index.js +415 -388
- package/package.json +16 -16
- package/packages/lsp-core/package.json +4 -0
- package/packages/lsp-core/src/index.ts +1 -0
- package/packages/lsp-core/src/lsp/cleanup-errors.test.ts +18 -0
- package/packages/lsp-core/src/lsp/cleanup-errors.ts +12 -3
- package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +261 -0
- package/packages/lsp-core/src/lsp/client-wrapper.test.ts +63 -0
- package/packages/lsp-core/src/lsp/client-wrapper.ts +35 -5
- package/packages/lsp-core/src/lsp/client.ts +262 -80
- package/packages/lsp-core/src/lsp/config-loader.ts +5 -17
- package/packages/lsp-core/src/lsp/connection.ts +12 -6
- package/packages/lsp-core/src/lsp/directory-diagnostics.test.ts +104 -0
- package/packages/lsp-core/src/lsp/directory-diagnostics.ts +60 -27
- package/packages/lsp-core/src/lsp/errors.ts +11 -0
- package/packages/lsp-core/src/lsp/fixtures/diagnostics-freshness-contract-probe.ts +283 -0
- package/packages/lsp-core/src/lsp/fixtures/workspace-edit-contract-probe.ts +196 -0
- package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +215 -0
- package/packages/lsp-core/src/lsp/formatters.ts +3 -0
- package/packages/lsp-core/src/lsp/json-rpc-connection-cancellation.test.ts +97 -0
- package/packages/lsp-core/src/lsp/json-rpc-connection.ts +73 -5
- package/packages/lsp-core/src/lsp/server-install-state.ts +3 -6
- package/packages/lsp-core/src/lsp/transport-protocol.ts +52 -0
- package/packages/lsp-core/src/lsp/transport.ts +96 -70
- package/packages/lsp-core/src/lsp/workspace-apply-edit-failure.ts +19 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit-lease.integration.test.ts +214 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit-sync.integration.test.ts +113 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit-test-support.ts +163 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit.integration.test.ts +163 -0
- package/packages/lsp-core/src/lsp/workspace-document-state.test.ts +67 -0
- package/packages/lsp-core/src/lsp/workspace-document-state.ts +368 -0
- package/packages/lsp-core/src/lsp/workspace-edit-adversarial.test.ts +113 -0
- package/packages/lsp-core/src/lsp/workspace-edit-commit.test.ts +140 -0
- package/packages/lsp-core/src/lsp/workspace-edit-commit.ts +220 -0
- package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.test.ts +56 -0
- package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.ts +30 -0
- package/packages/lsp-core/src/lsp/workspace-edit-fingerprint.ts +44 -0
- package/packages/lsp-core/src/lsp/workspace-edit-options.test.ts +147 -0
- package/packages/lsp-core/src/lsp/workspace-edit-parse-helpers.ts +59 -0
- package/packages/lsp-core/src/lsp/workspace-edit-parser.ts +130 -0
- package/packages/lsp-core/src/lsp/workspace-edit-path.ts +98 -0
- package/packages/lsp-core/src/lsp/workspace-edit-plan-types.ts +60 -0
- package/packages/lsp-core/src/lsp/workspace-edit-plan.ts +73 -0
- package/packages/lsp-core/src/lsp/workspace-edit-prevalidation.test.ts +174 -0
- package/packages/lsp-core/src/lsp/workspace-edit-resource-parser.ts +89 -0
- package/packages/lsp-core/src/lsp/workspace-edit-simulation.ts +183 -0
- package/packages/lsp-core/src/lsp/workspace-edit-snapshot.ts +53 -0
- package/packages/lsp-core/src/lsp/workspace-edit-text.ts +125 -0
- package/packages/lsp-core/src/lsp/workspace-edit-types.ts +121 -0
- package/packages/lsp-core/src/lsp/workspace-edit.characterization.test.ts +95 -0
- package/packages/lsp-core/src/lsp/workspace-edit.ts +49 -200
- package/packages/lsp-core/src/lsp/workspace-mutation-controller.ts +182 -0
- package/packages/lsp-core/src/mcp.ts +18 -7
- package/packages/lsp-core/src/missing-dependency-result.test.ts +105 -0
- package/packages/lsp-core/src/missing-dependency-result.ts +57 -0
- package/packages/lsp-core/src/post-edit/index.ts +1 -0
- package/packages/lsp-core/src/post-edit/orchestration.test.ts +157 -0
- package/packages/lsp-core/src/post-edit/orchestration.ts +178 -0
- package/packages/lsp-core/src/request-context.test.ts +171 -0
- package/packages/lsp-core/src/request-context.ts +222 -9
- package/packages/lsp-core/src/tool-surface.test.ts +4 -1
- package/packages/lsp-core/src/tools/diagnostics.ts +32 -13
- package/packages/lsp-core/src/tools/navigation.ts +12 -12
- package/packages/lsp-core/src/tools/rename.ts +10 -15
- package/packages/lsp-core/src/tools/symbols.ts +11 -11
- package/packages/lsp-core/src/tools/types.ts +2 -1
- package/packages/lsp-daemon/dist/cli.js +3114 -747
- package/packages/lsp-daemon/dist/client.d.ts +105 -0
- package/packages/lsp-daemon/dist/client.js +5851 -0
- package/packages/lsp-daemon/dist/daemon-client.d.ts +11 -6
- package/packages/lsp-daemon/dist/daemon-client.js +113 -30
- package/packages/lsp-daemon/dist/daemon-server.d.ts +1 -0
- package/packages/lsp-daemon/dist/daemon-server.js +40 -15
- package/packages/lsp-daemon/dist/ensure-daemon.d.ts +8 -7
- package/packages/lsp-daemon/dist/ensure-daemon.js +67 -44
- package/packages/lsp-daemon/dist/index.d.ts +2 -2
- package/packages/lsp-daemon/dist/index.js +2862 -754
- package/packages/lsp-daemon/dist/ipc-protocol.d.ts +46 -0
- package/packages/lsp-daemon/dist/ipc-protocol.js +187 -0
- package/packages/lsp-daemon/dist/lock.js +14 -4
- package/packages/lsp-daemon/dist/ownership.d.ts +49 -0
- package/packages/lsp-daemon/dist/ownership.js +168 -0
- package/packages/lsp-daemon/dist/paths.d.ts +33 -9
- package/packages/lsp-daemon/dist/paths.js +72 -33
- package/packages/lsp-daemon/dist/proxy.d.ts +3 -0
- package/packages/lsp-daemon/dist/proxy.js +54 -3
- package/packages/lsp-daemon/dist/request-routing.d.ts +7 -2
- package/packages/lsp-daemon/dist/request-routing.js +71 -22
- package/packages/lsp-daemon/dist/run-daemon.js +9 -2
- package/packages/lsp-daemon/dist/runtime-contract.d.ts +21 -0
- package/packages/lsp-daemon/dist/runtime-contract.js +58 -0
- package/packages/lsp-daemon/dist/socket-jsonrpc.js +6 -1
- package/packages/lsp-daemon/package.json +12 -3
- package/packages/lsp-tools-mcp/dist/cli.js +2115 -442
- package/packages/lsp-tools-mcp/dist/lsp/manager.js +1741 -148
- package/packages/lsp-tools-mcp/dist/mcp.js +2127 -454
- package/packages/lsp-tools-mcp/dist/request-context.js +176 -6
- package/packages/lsp-tools-mcp/dist/tools.js +2118 -446
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +55 -0
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +2959 -944
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook-cli.js +0 -4
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.d.ts +5 -2
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.js +41 -62
- package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.d.ts +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.js +24 -15
- package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.d.ts +3 -7
- package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.js +23 -49
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +3 -2
- package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.mjs +31 -1
- package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.test.mjs +76 -0
- package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.mjs +201 -0
- package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.test.mjs +55 -0
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +0 -4
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +49 -71
- package/packages/omo-codex/plugin/components/lsp/src/daemon-cli-path.ts +26 -15
- package/packages/omo-codex/plugin/components/lsp/src/lsp-session-state.ts +26 -64
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook-unavailable.test.ts +16 -17
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook.test.ts +30 -4
- package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +19 -5
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.5.md +1 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +1 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +6 -5
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +26 -14
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +2 -3
- package/packages/omo-codex/plugin/scripts/build-components.mjs +13 -1
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +1 -1
- package/packages/omo-codex/plugin/skills/start-work/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +6 -5
- package/packages/omo-codex/plugin/test/aggregate-build.test.mjs +8 -0
- package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +128 -15
- package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +10 -0
- package/packages/omo-codex/plugin/test/lsp-prebuild-layouts.test.mjs +2 -0
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +1 -1
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +328 -63
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { appendFileSync, readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
const [, , scenarioPath, eventsPath] = process.argv;
|
|
4
|
+
const scenario = JSON.parse(readFileSync(scenarioPath, "utf-8"));
|
|
5
|
+
|
|
6
|
+
let buffer = Buffer.alloc(0);
|
|
7
|
+
let nextServerRequestId = 10_000;
|
|
8
|
+
let renameIndex = 0;
|
|
9
|
+
let diagnosticIndex = 0;
|
|
10
|
+
const pendingServerRequests = new Map();
|
|
11
|
+
|
|
12
|
+
function record(event) {
|
|
13
|
+
appendFileSync(eventsPath, `${JSON.stringify(event)}\n`, "utf-8");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function send(message) {
|
|
17
|
+
const body = Buffer.from(JSON.stringify(message), "utf-8");
|
|
18
|
+
process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`);
|
|
19
|
+
process.stdout.write(body);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sendResponse(id, result) {
|
|
23
|
+
send({ jsonrpc: "2.0", id, result });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sendError(id, code, message) {
|
|
27
|
+
send({ jsonrpc: "2.0", id, error: { code, message } });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sendApplyEdit(edit, context) {
|
|
31
|
+
const id = nextServerRequestId;
|
|
32
|
+
nextServerRequestId += 1;
|
|
33
|
+
pendingServerRequests.set(String(id), context);
|
|
34
|
+
record({ type: "serverRequest", method: "workspace/applyEdit", id, edit });
|
|
35
|
+
send({ jsonrpc: "2.0", id, method: "workspace/applyEdit", params: { edit } });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function renameResult(step) {
|
|
39
|
+
if (step.renameResult === "same") return step.applyEdit ?? null;
|
|
40
|
+
if (step.renameResult === "different") return step.differentEdit ?? null;
|
|
41
|
+
if (step.renameResult === "edit") return step.renameEdit ?? null;
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function finishRename(context) {
|
|
46
|
+
const result = renameResult(context.step);
|
|
47
|
+
const delay = Number(context.step.responseDelayMs ?? 0);
|
|
48
|
+
setTimeout(() => {
|
|
49
|
+
record({ type: "serverResponse", method: "textDocument/rename", result });
|
|
50
|
+
sendResponse(context.renameRequestId, result);
|
|
51
|
+
}, delay);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function schedulePublish(step, fallbackUri, fallbackVersion) {
|
|
55
|
+
const delay = Number(step.delayMs ?? 0);
|
|
56
|
+
setTimeout(() => {
|
|
57
|
+
const params = {
|
|
58
|
+
uri: step.uri ?? fallbackUri,
|
|
59
|
+
...(step.version === undefined ? {} : { version: step.version }),
|
|
60
|
+
diagnostics: Array.isArray(step.diagnostics) ? step.diagnostics : [],
|
|
61
|
+
};
|
|
62
|
+
record({ type: "serverNotification", method: "textDocument/publishDiagnostics", params });
|
|
63
|
+
send({ jsonrpc: "2.0", method: "textDocument/publishDiagnostics", params });
|
|
64
|
+
}, delay);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function publishSteps(trigger) {
|
|
68
|
+
if (!Array.isArray(scenario.publishDiagnostics)) return [];
|
|
69
|
+
return scenario.publishDiagnostics.filter((step) => step?.trigger === trigger);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function applyPublishSteps(trigger, fallbackUri, fallbackVersion) {
|
|
73
|
+
for (const step of publishSteps(trigger)) schedulePublish(step, fallbackUri, fallbackVersion);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function handleClientResponse(message) {
|
|
77
|
+
const context = pendingServerRequests.get(String(message.id));
|
|
78
|
+
if (!context) return;
|
|
79
|
+
pendingServerRequests.delete(String(message.id));
|
|
80
|
+
record({
|
|
81
|
+
type: "clientResponse",
|
|
82
|
+
method: "workspace/applyEdit",
|
|
83
|
+
result: message.result ?? null,
|
|
84
|
+
error: message.error ?? null,
|
|
85
|
+
});
|
|
86
|
+
if (context.kind === "unscoped") return;
|
|
87
|
+
if (typeof context.pendingApplyResponses === "number") {
|
|
88
|
+
context.pendingApplyResponses -= 1;
|
|
89
|
+
if (context.pendingApplyResponses > 0) return;
|
|
90
|
+
finishRename(context);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (context.step.applyTwice === true && context.applyCount === 1) {
|
|
94
|
+
sendApplyEdit(context.step.applyEdit, { ...context, applyCount: 2 });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
finishRename(context);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function handleRequest(message) {
|
|
101
|
+
if (message.method === "initialize") {
|
|
102
|
+
record({ type: "clientRequest", method: message.method, id: message.id, params: message.params });
|
|
103
|
+
sendResponse(message.id, { capabilities: scenario.capabilities ?? {} });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (message.method === "textDocument/rename") {
|
|
107
|
+
const step = scenario.renameSteps?.[renameIndex] ?? {};
|
|
108
|
+
renameIndex += 1;
|
|
109
|
+
record({ type: "clientRequest", method: message.method, id: message.id, params: message.params, renameIndex });
|
|
110
|
+
if (step.applyEdit) {
|
|
111
|
+
const context = { kind: "rename", renameRequestId: message.id, step, applyCount: 1 };
|
|
112
|
+
if (step.applyConcurrently === true) {
|
|
113
|
+
context.pendingApplyResponses = 2;
|
|
114
|
+
sendApplyEdit(step.applyEdit, context);
|
|
115
|
+
sendApplyEdit(step.applyEdit, context);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const delay = Number(step.applyDelayMs ?? 0);
|
|
119
|
+
if (delay > 0) {
|
|
120
|
+
setTimeout(() => sendApplyEdit(step.applyEdit, context), delay);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
sendApplyEdit(step.applyEdit, context);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
finishRename({ renameRequestId: message.id, step });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (message.method === "textDocument/diagnostic") {
|
|
130
|
+
record({ type: "clientRequest", method: message.method, id: message.id, params: message.params, diagnosticIndex: diagnosticIndex + 1 });
|
|
131
|
+
applyPublishSteps("diagnosticRequest", message.params?.textDocument?.uri, undefined);
|
|
132
|
+
const step = Array.isArray(scenario.diagnosticResponses) ? scenario.diagnosticResponses[diagnosticIndex] : undefined;
|
|
133
|
+
diagnosticIndex += 1;
|
|
134
|
+
if (step?.action === "hang") return;
|
|
135
|
+
if (step?.action === "close") {
|
|
136
|
+
const delay = Number(step.delayMs ?? 0);
|
|
137
|
+
setTimeout(() => process.exit(0), delay);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const delay = Number(step?.delayMs ?? 0);
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
if (step?.error) {
|
|
143
|
+
record({ type: "serverError", method: "textDocument/diagnostic", error: step.error });
|
|
144
|
+
sendError(message.id, step.error.code, step.error.message);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const result = step?.report ?? { items: scenario.diagnostics ?? [] };
|
|
148
|
+
record({ type: "serverResponse", method: "textDocument/diagnostic", result });
|
|
149
|
+
sendResponse(message.id, result);
|
|
150
|
+
}, delay);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (message.method === "shutdown") {
|
|
154
|
+
sendResponse(message.id, null);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
sendResponse(message.id, null);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function handleNotification(message) {
|
|
161
|
+
record({ type: "clientNotification", method: message.method, params: message.params ?? null });
|
|
162
|
+
if (message.method === "initialized" && scenario.unscopedApplyEdit) {
|
|
163
|
+
sendApplyEdit(scenario.unscopedApplyEdit, { kind: "unscoped" });
|
|
164
|
+
}
|
|
165
|
+
if (message.method === "initialized") {
|
|
166
|
+
applyPublishSteps("initialized", undefined, undefined);
|
|
167
|
+
}
|
|
168
|
+
if (message.method === "textDocument/didOpen") {
|
|
169
|
+
applyPublishSteps("didOpen", message.params.textDocument.uri, message.params.textDocument.version);
|
|
170
|
+
}
|
|
171
|
+
if (message.method === "textDocument/didChange" && Array.isArray(scenario.diagnostics)) {
|
|
172
|
+
send({
|
|
173
|
+
jsonrpc: "2.0",
|
|
174
|
+
method: "textDocument/publishDiagnostics",
|
|
175
|
+
params: {
|
|
176
|
+
uri: message.params.textDocument.uri,
|
|
177
|
+
version: message.params.textDocument.version,
|
|
178
|
+
diagnostics: scenario.diagnostics,
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
if (message.method === "textDocument/didChange") {
|
|
183
|
+
applyPublishSteps("didChange", message.params.textDocument.uri, message.params.textDocument.version);
|
|
184
|
+
}
|
|
185
|
+
if (message.method === "exit") process.exit(0);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function handleMessage(message) {
|
|
189
|
+
if (Object.hasOwn(message, "id") && (Object.hasOwn(message, "result") || Object.hasOwn(message, "error"))) {
|
|
190
|
+
handleClientResponse(message);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (Object.hasOwn(message, "id")) {
|
|
194
|
+
handleRequest(message);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
handleNotification(message);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
process.stdin.on("data", (chunk) => {
|
|
201
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
202
|
+
for (;;) {
|
|
203
|
+
const headerEnd = buffer.indexOf("\r\n\r\n");
|
|
204
|
+
if (headerEnd === -1) return;
|
|
205
|
+
const header = buffer.subarray(0, headerEnd).toString("ascii");
|
|
206
|
+
const match = /content-length:\s*(\d+)/i.exec(header);
|
|
207
|
+
if (!match) process.exit(2);
|
|
208
|
+
const length = Number(match[1]);
|
|
209
|
+
const bodyStart = headerEnd + 4;
|
|
210
|
+
if (buffer.length < bodyStart + length) return;
|
|
211
|
+
const body = buffer.subarray(bodyStart, bodyStart + length).toString("utf-8");
|
|
212
|
+
buffer = buffer.subarray(bodyStart + length);
|
|
213
|
+
handleMessage(JSON.parse(body));
|
|
214
|
+
}
|
|
215
|
+
});
|
|
@@ -127,6 +127,9 @@ export function formatApplyResult(result: ApplyResult): string {
|
|
|
127
127
|
for (const file of result.filesModified) {
|
|
128
128
|
lines.push(` - ${file}`);
|
|
129
129
|
}
|
|
130
|
+
if (result.lateAbort) {
|
|
131
|
+
lines.push("Cancellation arrived after the filesystem commit began; the committed edit completed.");
|
|
132
|
+
}
|
|
130
133
|
} else {
|
|
131
134
|
lines.push("Failed to apply some changes:");
|
|
132
135
|
for (const err of result.errors) {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { PassThrough } from "node:stream";
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "bun:test";
|
|
4
|
+
|
|
5
|
+
import { JsonRpcConnection } from "./json-rpc-connection.js";
|
|
6
|
+
|
|
7
|
+
type JsonMessage = Record<string, unknown>;
|
|
8
|
+
|
|
9
|
+
function messageId(message: JsonMessage): string | number | null | undefined {
|
|
10
|
+
const id = message["id"];
|
|
11
|
+
return typeof id === "string" || typeof id === "number" || id === null ? id : undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function cancelTarget(message: JsonMessage): string | number | undefined {
|
|
15
|
+
const params = message["params"];
|
|
16
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) return undefined;
|
|
17
|
+
const id = Reflect.get(params, "id");
|
|
18
|
+
return typeof id === "string" || typeof id === "number" ? id : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function encodeMessage(message: JsonMessage): string {
|
|
22
|
+
const body = JSON.stringify(message);
|
|
23
|
+
return `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function recordJsonRpcMessages(stream: PassThrough) {
|
|
27
|
+
const messages: JsonMessage[] = [];
|
|
28
|
+
const waiters: Array<() => void> = [];
|
|
29
|
+
let buffer = Buffer.alloc(0);
|
|
30
|
+
stream.on("data", (chunk: Buffer) => {
|
|
31
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
32
|
+
for (;;) {
|
|
33
|
+
const headerEnd = buffer.indexOf("\r\n\r\n");
|
|
34
|
+
if (headerEnd === -1) return;
|
|
35
|
+
const header = buffer.subarray(0, headerEnd).toString("ascii");
|
|
36
|
+
const match = /content-length:\s*(\d+)/i.exec(header);
|
|
37
|
+
if (!match?.[1]) throw new Error("missing content-length");
|
|
38
|
+
const bodyStart = headerEnd + 4;
|
|
39
|
+
const bodyEnd = bodyStart + Number.parseInt(match[1], 10);
|
|
40
|
+
if (buffer.length < bodyEnd) return;
|
|
41
|
+
const parsed: unknown = JSON.parse(buffer.subarray(bodyStart, bodyEnd).toString("utf8"));
|
|
42
|
+
buffer = buffer.subarray(bodyEnd);
|
|
43
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
44
|
+
messages.push(Object.fromEntries(Object.entries(parsed)));
|
|
45
|
+
}
|
|
46
|
+
for (const wake of waiters.splice(0)) wake();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
messages,
|
|
51
|
+
waitForCount(count: number): Promise<void> {
|
|
52
|
+
if (messages.length >= count) return Promise.resolve();
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const timer = setTimeout(() => {
|
|
55
|
+
reject(new Error(`waited for ${count} JSON-RPC messages, observed ${messages.length}`));
|
|
56
|
+
}, 1_000);
|
|
57
|
+
const wake = (): void => {
|
|
58
|
+
if (messages.length < count) {
|
|
59
|
+
waiters.push(wake);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
resolve();
|
|
64
|
+
};
|
|
65
|
+
waiters.push(wake);
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe("JsonRpcConnection cancellation", () => {
|
|
72
|
+
it("#given an active request #when its signal aborts #then cancel uses the same id and late responses are ignored", async () => {
|
|
73
|
+
const serverToClient = new PassThrough();
|
|
74
|
+
const clientToServer = new PassThrough();
|
|
75
|
+
const recorder = recordJsonRpcMessages(clientToServer);
|
|
76
|
+
const connection = new JsonRpcConnection(serverToClient, clientToServer);
|
|
77
|
+
connection.listen();
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
|
|
80
|
+
const request = connection.sendRequest("textDocument/diagnostic", { textDocument: { uri: "file:///a.ts" } }, {
|
|
81
|
+
signal: controller.signal,
|
|
82
|
+
});
|
|
83
|
+
await recorder.waitForCount(1);
|
|
84
|
+
const requestId = messageId(recorder.messages[0] ?? {});
|
|
85
|
+
expect(typeof requestId === "string" || typeof requestId === "number").toBe(true);
|
|
86
|
+
|
|
87
|
+
controller.abort(new Error("caller cancelled"));
|
|
88
|
+
await recorder.waitForCount(2);
|
|
89
|
+
await expect(request).rejects.toThrow("caller cancelled");
|
|
90
|
+
expect(connection.pendingRequestCount()).toBe(0);
|
|
91
|
+
expect(recorder.messages[1]?.["method"]).toBe("$/cancelRequest");
|
|
92
|
+
expect(cancelTarget(recorder.messages[1] ?? {})).toBe(typeof requestId === "number" || typeof requestId === "string" ? requestId : undefined);
|
|
93
|
+
|
|
94
|
+
serverToClient.write(encodeMessage({ jsonrpc: "2.0", id: requestId ?? null, result: { items: [] } }));
|
|
95
|
+
expect(connection.pendingRequestCount()).toBe(0);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -3,11 +3,16 @@ type JsonRpcId = number | string | null;
|
|
|
3
3
|
interface PendingRequest {
|
|
4
4
|
resolve(result: unknown): void;
|
|
5
5
|
reject(error: Error): void;
|
|
6
|
+
cleanup(): void;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
type NotificationHandler = (params: unknown) => void;
|
|
9
10
|
type RequestHandler = (params: unknown) => Promise<unknown> | unknown;
|
|
10
11
|
|
|
12
|
+
export interface JsonRpcRequestOptions {
|
|
13
|
+
readonly signal?: AbortSignal;
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
const HEADER_SEPARATOR = "\r\n\r\n";
|
|
12
17
|
const PARSE_ERROR = -32700;
|
|
13
18
|
const INVALID_REQUEST = -32600;
|
|
@@ -56,32 +61,81 @@ export class JsonRpcConnection {
|
|
|
56
61
|
this.errorHandlers.push(handler);
|
|
57
62
|
}
|
|
58
63
|
|
|
59
|
-
async sendRequest<T>(method: string, params?: unknown): Promise<T> {
|
|
64
|
+
async sendRequest<T>(method: string, params?: unknown, options: JsonRpcRequestOptions = {}): Promise<T> {
|
|
60
65
|
if (this.disposed) throw new Error("JSON-RPC connection is disposed");
|
|
61
66
|
|
|
62
67
|
const id = this.nextRequestId;
|
|
63
68
|
this.nextRequestId += 1;
|
|
69
|
+
const key = String(id);
|
|
64
70
|
const message = params === undefined ? { jsonrpc: "2.0", id, method } : { jsonrpc: "2.0", id, method, params };
|
|
71
|
+
let requestWritten = false;
|
|
72
|
+
let cancelAfterWrite = false;
|
|
73
|
+
let settled = false;
|
|
74
|
+
const writeCancel = (): Promise<void> => this.writeMessage({ jsonrpc: "2.0", method: "$/cancelRequest", params: { id } });
|
|
65
75
|
|
|
66
76
|
const responsePromise = new Promise<T>((resolve, reject) => {
|
|
67
|
-
|
|
77
|
+
const cleanup = (): void => {
|
|
78
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
79
|
+
};
|
|
80
|
+
const settleCancel = (): void => {
|
|
81
|
+
if (settled) return;
|
|
82
|
+
settled = true;
|
|
83
|
+
this.pendingRequests.delete(key);
|
|
84
|
+
cleanup();
|
|
85
|
+
const rejectCancelled = (): void => reject(abortError(options.signal));
|
|
86
|
+
if (!requestWritten) {
|
|
87
|
+
cancelAfterWrite = true;
|
|
88
|
+
rejectCancelled();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
void writeCancel().then(rejectCancelled, (error) => {
|
|
92
|
+
this.emitError(toError(error));
|
|
93
|
+
rejectCancelled();
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
const onAbort = (): void => settleCancel();
|
|
97
|
+
this.pendingRequests.set(key, {
|
|
68
98
|
resolve(result) {
|
|
99
|
+
settled = true;
|
|
100
|
+
cleanup();
|
|
69
101
|
resolve(result as T);
|
|
70
102
|
},
|
|
71
|
-
reject
|
|
103
|
+
reject(error) {
|
|
104
|
+
settled = true;
|
|
105
|
+
cleanup();
|
|
106
|
+
reject(error);
|
|
107
|
+
},
|
|
108
|
+
cleanup,
|
|
72
109
|
});
|
|
110
|
+
if (options.signal?.aborted) {
|
|
111
|
+
settleCancel();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
73
115
|
});
|
|
74
116
|
|
|
117
|
+
if (settled) return responsePromise;
|
|
75
118
|
try {
|
|
76
119
|
await this.writeMessage(message);
|
|
120
|
+
requestWritten = true;
|
|
121
|
+
if (cancelAfterWrite) await writeCancel();
|
|
77
122
|
} catch (error) {
|
|
78
|
-
|
|
123
|
+
if (settled) return responsePromise;
|
|
124
|
+
const pending = this.pendingRequests.get(key);
|
|
125
|
+
if (pending) {
|
|
126
|
+
pending.cleanup();
|
|
127
|
+
this.pendingRequests.delete(key);
|
|
128
|
+
}
|
|
79
129
|
throw error;
|
|
80
130
|
}
|
|
81
131
|
|
|
82
132
|
return responsePromise;
|
|
83
133
|
}
|
|
84
134
|
|
|
135
|
+
pendingRequestCount(): number {
|
|
136
|
+
return this.pendingRequests.size;
|
|
137
|
+
}
|
|
138
|
+
|
|
85
139
|
async sendNotification(method: string, params?: unknown): Promise<void> {
|
|
86
140
|
if (this.disposed) return;
|
|
87
141
|
const message = params === undefined ? { jsonrpc: "2.0", method } : { jsonrpc: "2.0", method, params };
|
|
@@ -97,6 +151,7 @@ export class JsonRpcConnection {
|
|
|
97
151
|
this.reader.off("error", this.handleStreamError);
|
|
98
152
|
this.writer.off("error", this.handleStreamError);
|
|
99
153
|
for (const pending of this.pendingRequests.values()) {
|
|
154
|
+
pending.cleanup();
|
|
100
155
|
pending.reject(new Error("JSON-RPC connection disposed"));
|
|
101
156
|
}
|
|
102
157
|
this.pendingRequests.clear();
|
|
@@ -188,6 +243,7 @@ export class JsonRpcConnection {
|
|
|
188
243
|
const pending = this.pendingRequests.get(String(id));
|
|
189
244
|
if (!pending) return;
|
|
190
245
|
this.pendingRequests.delete(String(id));
|
|
246
|
+
pending.cleanup();
|
|
191
247
|
|
|
192
248
|
if ("error" in message) {
|
|
193
249
|
pending.reject(jsonRpcErrorToError(message["error"]));
|
|
@@ -203,7 +259,11 @@ export class JsonRpcConnection {
|
|
|
203
259
|
try {
|
|
204
260
|
handler(params);
|
|
205
261
|
} catch (error) {
|
|
206
|
-
|
|
262
|
+
if (error instanceof Error) {
|
|
263
|
+
this.emitError(error);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
this.emitError(new Error(String(error)));
|
|
207
267
|
}
|
|
208
268
|
}
|
|
209
269
|
|
|
@@ -259,6 +319,14 @@ export class JsonRpcConnection {
|
|
|
259
319
|
}
|
|
260
320
|
}
|
|
261
321
|
|
|
322
|
+
function abortError(signal: AbortSignal | undefined): Error {
|
|
323
|
+
const reason = signal?.reason;
|
|
324
|
+
if (reason instanceof Error) return reason;
|
|
325
|
+
const error = new Error(typeof reason === "string" ? reason : "LSP request cancelled");
|
|
326
|
+
error.name = "AbortError";
|
|
327
|
+
return error;
|
|
328
|
+
}
|
|
329
|
+
|
|
262
330
|
function parseContentLength(headers: string): number | null {
|
|
263
331
|
for (const line of headers.split("\r\n")) {
|
|
264
332
|
const separatorIndex = line.indexOf(":");
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
import { dirname, isAbsolute, join } from "node:path";
|
|
2
|
+
import { dirname } from "node:path";
|
|
4
3
|
|
|
5
|
-
import {
|
|
4
|
+
import { lspRequestContext } from "../request-context.js";
|
|
6
5
|
|
|
7
6
|
export type InstallDecision = "declined" | "allowed";
|
|
8
7
|
|
|
@@ -14,9 +13,7 @@ export interface InstallDecisionRecord {
|
|
|
14
13
|
type InstallDecisions = Record<string, InstallDecisionRecord>;
|
|
15
14
|
|
|
16
15
|
export function getInstallDecisionsPath(): string {
|
|
17
|
-
|
|
18
|
-
if (!override) return join(homedir(), ".codex", "lsp-install-decisions.json");
|
|
19
|
-
return isAbsolute(override) ? override : join(homedir(), override);
|
|
16
|
+
return lspRequestContext().installDecisionsPath;
|
|
20
17
|
}
|
|
21
18
|
|
|
22
19
|
export function loadInstallDecisions(): InstallDecisions {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Diagnostic } from "./types.js";
|
|
2
|
+
|
|
3
|
+
interface ConfigurationItem {
|
|
4
|
+
readonly section?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface DiagnosticsParams {
|
|
8
|
+
readonly uri: string;
|
|
9
|
+
readonly diagnostics: Diagnostic[];
|
|
10
|
+
readonly version?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
14
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function parseConfigurationItems(params: unknown): ConfigurationItem[] {
|
|
18
|
+
if (!isRecord(params) || !Array.isArray(params["items"])) return [];
|
|
19
|
+
const items: ConfigurationItem[] = [];
|
|
20
|
+
for (const item of params["items"]) {
|
|
21
|
+
if (!isRecord(item)) continue;
|
|
22
|
+
const section = item["section"];
|
|
23
|
+
items.push(section === undefined || typeof section !== "string" ? {} : { section });
|
|
24
|
+
}
|
|
25
|
+
return items;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseDiagnosticsParams(params: unknown): DiagnosticsParams | null {
|
|
29
|
+
if (!isRecord(params) || typeof params["uri"] !== "string") return null;
|
|
30
|
+
const diagnostics = Array.isArray(params["diagnostics"]) ? params["diagnostics"].filter(isDiagnostic) : [];
|
|
31
|
+
const version = typeof params["version"] === "number" ? params["version"] : undefined;
|
|
32
|
+
return { uri: params["uri"], diagnostics, ...(version === undefined ? {} : { version }) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createLspSpawnEnv(
|
|
36
|
+
_root: string,
|
|
37
|
+
input: Record<string, string | undefined>,
|
|
38
|
+
): Record<string, string | undefined> {
|
|
39
|
+
return { ...input };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isDiagnostic(value: unknown): value is Diagnostic {
|
|
43
|
+
return isRecord(value) && isRange(value["range"]) && typeof value["message"] === "string";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isRange(value: unknown): value is Diagnostic["range"] {
|
|
47
|
+
return isRecord(value) && isPosition(value["start"]) && isPosition(value["end"]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isPosition(value: unknown): value is Diagnostic["range"]["start"] {
|
|
51
|
+
return isRecord(value) && typeof value["line"] === "number" && typeof value["character"] === "number";
|
|
52
|
+
}
|