oh-my-opencode 5.0.0-beta.21 → 5.0.0-beta.23
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/dist/cli/index.js +290 -288
- package/dist/cli-node/index.js +290 -288
- package/dist/index.js +1675 -1657
- package/dist/skills/debugging/SKILL.md +2 -0
- package/dist/skills/debugging/references/methodology/00-setup.md +12 -0
- package/dist/skills/debugging/references/methodology/02-investigate.md +21 -0
- package/dist/skills/debugging/references/runtimes/go.md +15 -1
- package/dist/skills/debugging/references/runtimes/native-binary.md +38 -0
- package/dist/skills/debugging/references/runtimes/node.md +25 -0
- package/dist/skills/debugging/references/runtimes/python.md +13 -0
- package/dist/skills/debugging/references/runtimes/rust.md +41 -0
- package/dist/skills/debugging/references/scripts/dap.mjs +267 -0
- package/dist/skills/debugging/references/scripts/dap.test.ts +86 -0
- package/dist/skills/debugging/references/scripts/fixture-adapter.mjs +59 -0
- package/dist/skills/debugging/references/tools/dap.md +103 -0
- package/dist/skills/debugging/references/tools/frida.md +193 -0
- package/dist/tools/delegate-task/types.d.ts +3 -0
- package/dist/tui.js +131 -147
- package/package.json +18 -15
- package/packages/lsp-core/src/index.ts +1 -0
- package/packages/lsp-core/src/lsp/client-wrapper.ts +25 -5
- package/packages/lsp-core/src/lsp/client.ts +26 -0
- package/packages/lsp-core/src/lsp/connection.ts +16 -0
- package/packages/lsp-core/src/lsp/constants.ts +1 -0
- package/packages/lsp-core/src/lsp/format-document.test.ts +143 -0
- package/packages/lsp-core/src/lsp/format-document.ts +90 -0
- package/packages/lsp-core/src/lsp/manager-max-clients.test.ts +151 -0
- package/packages/lsp-core/src/lsp/manager.ts +34 -1
- package/packages/lsp-core/src/lsp/server-definitions.ts +29 -0
- package/packages/lsp-core/src/lsp/server-installation.test.ts +376 -0
- package/packages/lsp-core/src/lsp/server-installation.ts +200 -27
- package/packages/lsp-core/src/lsp/server-resolution-local-binary.test.ts +170 -0
- package/packages/lsp-core/src/lsp/server-resolution.ts +18 -5
- package/packages/lsp-core/src/lsp/transport.ts +9 -0
- package/packages/lsp-core/src/lsp/types.ts +8 -0
- package/packages/lsp-core/src/tool-surface.test.ts +13 -1
- package/packages/lsp-core/src/tools/definitions.ts +14 -0
- package/packages/lsp-core/src/tools/format.test.ts +56 -0
- package/packages/lsp-core/src/tools/format.ts +56 -0
- package/packages/lsp-core/src/tools/index.ts +1 -0
- package/packages/lsp-core/src/tools/types.ts +10 -0
- package/packages/lsp-daemon/dist/cli.js +432 -144
- package/packages/lsp-daemon/dist/client.d.ts +10 -0
- package/packages/lsp-daemon/dist/client.js +401 -107
- package/packages/lsp-daemon/dist/daemon-client.d.ts +1 -0
- package/packages/lsp-daemon/dist/daemon-client.js +3 -0
- package/packages/lsp-daemon/dist/index.js +395 -107
- package/packages/lsp-daemon/package.json +1 -1
- package/packages/lsp-tools-mcp/dist/cli.js +384 -99
- package/packages/lsp-tools-mcp/dist/lsp/manager.js +53 -0
- package/packages/lsp-tools-mcp/dist/mcp.js +384 -99
- package/packages/lsp-tools-mcp/dist/tools.js +385 -99
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +2 -2
- 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/dist/cli.js +69 -35
- package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +21 -2
- 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 +3 -3
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +404 -121
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/dist/cli.js +3 -3
- 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/rules/src/post-compact-budget.ts +3 -3
- package/packages/omo-codex/plugin/components/rules/test/post-compact-budget.test.ts +4 -4
- 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/dist/cli.js +30 -15
- package/packages/omo-codex/plugin/components/telemetry/dist/posthog.js +30 -15
- 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-execute-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-commands.js +5 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.d.ts +2 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.js +11 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +22 -10
- 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/src/cli-commands.ts +5 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts +13 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli.ts +3 -5
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-entrypoint.test.ts +26 -2
- 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-ulw-execute-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-ulw-execute-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/model-catalog.json +3 -3
- package/packages/omo-codex/plugin/package-lock.json +13 -13
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/migrate-codex-config/catalog.mjs +3 -3
- package/packages/omo-codex/plugin/skills/debugging/SKILL.md +2 -0
- package/packages/omo-codex/plugin/skills/debugging/references/methodology/00-setup.md +12 -0
- package/packages/omo-codex/plugin/skills/debugging/references/methodology/02-investigate.md +21 -0
- package/packages/omo-codex/plugin/skills/debugging/references/runtimes/go.md +15 -1
- package/packages/omo-codex/plugin/skills/debugging/references/runtimes/native-binary.md +38 -0
- package/packages/omo-codex/plugin/skills/debugging/references/runtimes/node.md +25 -0
- package/packages/omo-codex/plugin/skills/debugging/references/runtimes/python.md +13 -0
- package/packages/omo-codex/plugin/skills/debugging/references/runtimes/rust.md +41 -0
- package/packages/omo-codex/plugin/skills/debugging/references/scripts/dap.mjs +267 -0
- package/packages/omo-codex/plugin/skills/debugging/references/scripts/fixture-adapter.mjs +59 -0
- package/packages/omo-codex/plugin/skills/debugging/references/tools/dap.md +103 -0
- package/packages/omo-codex/plugin/skills/debugging/references/tools/frida.md +193 -0
- package/packages/omo-codex/plugin/test/aggregate-model-catalog.test.mjs +1 -1
- package/packages/omo-codex/plugin/test/auto-update.test.mjs +2 -2
- package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +8 -8
- package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +2 -2
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +38 -23
- package/packages/shared-skills/skills/debugging/SKILL.md +2 -0
- package/packages/shared-skills/skills/debugging/references/methodology/00-setup.md +12 -0
- package/packages/shared-skills/skills/debugging/references/methodology/02-investigate.md +21 -0
- package/packages/shared-skills/skills/debugging/references/runtimes/go.md +15 -1
- package/packages/shared-skills/skills/debugging/references/runtimes/native-binary.md +38 -0
- package/packages/shared-skills/skills/debugging/references/runtimes/node.md +25 -0
- package/packages/shared-skills/skills/debugging/references/runtimes/python.md +13 -0
- package/packages/shared-skills/skills/debugging/references/runtimes/rust.md +41 -0
- package/packages/shared-skills/skills/debugging/references/scripts/dap.mjs +267 -0
- package/packages/shared-skills/skills/debugging/references/scripts/dap.test.ts +86 -0
- package/packages/shared-skills/skills/debugging/references/scripts/fixture-adapter.mjs +59 -0
- package/packages/shared-skills/skills/debugging/references/tools/dap.md +103 -0
- package/packages/shared-skills/skills/debugging/references/tools/frida.md +193 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
__resetServerBinaryResolutionCacheForTests,
|
|
8
|
+
__serverBinaryProbeCountForTests,
|
|
9
|
+
isServerInstalled,
|
|
10
|
+
resolveServerBinary,
|
|
11
|
+
} from "./server-installation.js";
|
|
12
|
+
|
|
13
|
+
const tempDirectories: string[] = [];
|
|
14
|
+
let previousPath: string | undefined;
|
|
15
|
+
|
|
16
|
+
function makeTempRoot(prefix: string): string {
|
|
17
|
+
const root = mkdtempSync(join(tmpdir(), prefix));
|
|
18
|
+
tempDirectories.push(root);
|
|
19
|
+
return root;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writeExecutable(path: string, body = "#!/bin/sh\nexit 0\n"): string {
|
|
23
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
24
|
+
writeFileSync(path, body);
|
|
25
|
+
chmodSync(path, 0o755);
|
|
26
|
+
return path;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
previousPath = process.env["PATH"];
|
|
31
|
+
__resetServerBinaryResolutionCacheForTests();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
if (previousPath === undefined) delete process.env["PATH"];
|
|
36
|
+
else process.env["PATH"] = previousPath;
|
|
37
|
+
for (const directory of tempDirectories.splice(0)) {
|
|
38
|
+
rmSync(directory, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
__resetServerBinaryResolutionCacheForTests();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("resolveServerBinary node marker resolution", () => {
|
|
44
|
+
test("#given a package.json marker and a local node bin #when resolving with an empty PATH #then it returns the local absolute path", () => {
|
|
45
|
+
// given
|
|
46
|
+
const root = makeTempRoot("lsp-local-node-");
|
|
47
|
+
writeFileSync(join(root, "package.json"), "{}\n");
|
|
48
|
+
const binaryPath = writeExecutable(join(root, "node_modules", ".bin", "biome"));
|
|
49
|
+
process.env["PATH"] = join(root, "definitely-empty");
|
|
50
|
+
|
|
51
|
+
// when
|
|
52
|
+
const resolved = resolveServerBinary(["biome", "lsp-proxy", "--stdio"], root);
|
|
53
|
+
|
|
54
|
+
// then
|
|
55
|
+
expect(resolved).toBe(binaryPath);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("#given a nested working directory #when resolving #then it walks up to the marker root", () => {
|
|
59
|
+
// given
|
|
60
|
+
const root = makeTempRoot("lsp-local-nested-");
|
|
61
|
+
writeFileSync(join(root, "package.json"), "{}\n");
|
|
62
|
+
const binaryPath = writeExecutable(join(root, "node_modules", ".bin", "biome"));
|
|
63
|
+
const nested = join(root, "packages", "deep", "src");
|
|
64
|
+
mkdirSync(nested, { recursive: true });
|
|
65
|
+
process.env["PATH"] = join(root, "definitely-empty");
|
|
66
|
+
|
|
67
|
+
// when
|
|
68
|
+
const resolved = resolveServerBinary(["biome", "lsp-proxy", "--stdio"], nested);
|
|
69
|
+
|
|
70
|
+
// then
|
|
71
|
+
expect(resolved).toBe(binaryPath);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("#given a local bin without any project marker #when resolving #then the unmarked directory is not trusted", () => {
|
|
75
|
+
// given
|
|
76
|
+
const root = makeTempRoot("lsp-local-unmarked-");
|
|
77
|
+
writeExecutable(join(root, "node_modules", ".bin", "biome"));
|
|
78
|
+
process.env["PATH"] = join(root, "definitely-empty");
|
|
79
|
+
|
|
80
|
+
// when
|
|
81
|
+
const resolved = resolveServerBinary(["biome", "lsp-proxy", "--stdio"], root);
|
|
82
|
+
|
|
83
|
+
// then
|
|
84
|
+
expect(resolved).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("#given a bun.lock marker #when resolving #then the node bin directory is probed", () => {
|
|
88
|
+
// given
|
|
89
|
+
const root = makeTempRoot("lsp-local-bunlock-");
|
|
90
|
+
writeFileSync(join(root, "bun.lock"), "");
|
|
91
|
+
const binaryPath = writeExecutable(join(root, "node_modules", ".bin", "biome"));
|
|
92
|
+
process.env["PATH"] = join(root, "definitely-empty");
|
|
93
|
+
|
|
94
|
+
// when / then
|
|
95
|
+
expect(resolveServerBinary(["biome"], root)).toBe(binaryPath);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("resolveServerBinary non-node ecosystems", () => {
|
|
100
|
+
test("#given a pyproject marker and a venv binary #when resolving #then it returns the venv path", () => {
|
|
101
|
+
// given
|
|
102
|
+
const root = makeTempRoot("lsp-local-python-");
|
|
103
|
+
writeFileSync(join(root, "pyproject.toml"), "[project]\n");
|
|
104
|
+
const binaryPath = writeExecutable(join(root, ".venv", "bin", "ruff"));
|
|
105
|
+
process.env["PATH"] = join(root, "definitely-empty");
|
|
106
|
+
|
|
107
|
+
// when / then
|
|
108
|
+
expect(resolveServerBinary(["ruff", "server"], root)).toBe(binaryPath);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("#given a Gemfile marker and a bundled binary #when resolving #then it returns the vendored bundle path", () => {
|
|
112
|
+
// given
|
|
113
|
+
const root = makeTempRoot("lsp-local-ruby-");
|
|
114
|
+
writeFileSync(join(root, "Gemfile"), "source 'https://rubygems.org'\n");
|
|
115
|
+
const binaryPath = writeExecutable(join(root, "vendor", "bundle", "bin", "rubocop"));
|
|
116
|
+
process.env["PATH"] = join(root, "definitely-empty");
|
|
117
|
+
|
|
118
|
+
// when / then
|
|
119
|
+
expect(resolveServerBinary(["rubocop", "--lsp"], root)).toBe(binaryPath);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("#given a go.mod marker and a project bin #when resolving #then it returns the project bin path", () => {
|
|
123
|
+
// given
|
|
124
|
+
const root = makeTempRoot("lsp-local-go-");
|
|
125
|
+
writeFileSync(join(root, "go.mod"), "module example.com/demo\n");
|
|
126
|
+
const binaryPath = writeExecutable(join(root, "bin", "gopls"));
|
|
127
|
+
process.env["PATH"] = join(root, "definitely-empty");
|
|
128
|
+
|
|
129
|
+
// when / then
|
|
130
|
+
expect(resolveServerBinary(["gopls"], root)).toBe(binaryPath);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("#given a python marker without a matching venv binary #when resolving #then node bin dirs are not borrowed", () => {
|
|
134
|
+
// given
|
|
135
|
+
const root = makeTempRoot("lsp-local-python-miss-");
|
|
136
|
+
writeFileSync(join(root, "requirements.txt"), "ruff\n");
|
|
137
|
+
writeExecutable(join(root, "node_modules", ".bin", "ruff"));
|
|
138
|
+
process.env["PATH"] = join(root, "definitely-empty");
|
|
139
|
+
|
|
140
|
+
// when / then
|
|
141
|
+
expect(resolveServerBinary(["ruff", "server"], root)).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("resolveServerBinary probe ordering", () => {
|
|
146
|
+
test("#given a local binary and a PATH binary #when resolving #then the local binary wins", () => {
|
|
147
|
+
// given
|
|
148
|
+
const root = makeTempRoot("lsp-local-priority-");
|
|
149
|
+
writeFileSync(join(root, "package.json"), "{}\n");
|
|
150
|
+
const localBinary = writeExecutable(join(root, "node_modules", ".bin", "biome"));
|
|
151
|
+
const pathDir = makeTempRoot("lsp-path-priority-");
|
|
152
|
+
writeExecutable(join(pathDir, "biome"));
|
|
153
|
+
process.env["PATH"] = pathDir;
|
|
154
|
+
|
|
155
|
+
// when / then
|
|
156
|
+
expect(resolveServerBinary(["biome"], root)).toBe(localBinary);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("#given only a PATH binary #when resolving #then the PATH entry is returned", () => {
|
|
160
|
+
// given
|
|
161
|
+
const root = makeTempRoot("lsp-path-fallback-root-");
|
|
162
|
+
writeFileSync(join(root, "package.json"), "{}\n");
|
|
163
|
+
const pathDir = makeTempRoot("lsp-path-fallback-");
|
|
164
|
+
const pathBinary = writeExecutable(join(pathDir, "biome"));
|
|
165
|
+
process.env["PATH"] = pathDir;
|
|
166
|
+
|
|
167
|
+
// when / then
|
|
168
|
+
expect(resolveServerBinary(["biome"], root)).toBe(pathBinary);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("#given a path-shaped command #when resolving #then the command path itself is validated", () => {
|
|
172
|
+
// given
|
|
173
|
+
const root = makeTempRoot("lsp-explicit-path-");
|
|
174
|
+
const binaryPath = writeExecutable(join(root, "tools", "custom-ls"));
|
|
175
|
+
process.env["PATH"] = join(root, "definitely-empty");
|
|
176
|
+
|
|
177
|
+
// when / then
|
|
178
|
+
expect(resolveServerBinary([binaryPath, "--stdio"], root)).toBe(binaryPath);
|
|
179
|
+
expect(resolveServerBinary([join(root, "tools", "absent-ls")], root)).toBeNull();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("#given no working directory #when resolving #then it falls back to PATH only", () => {
|
|
183
|
+
// given
|
|
184
|
+
const pathDir = makeTempRoot("lsp-no-cwd-");
|
|
185
|
+
const pathBinary = writeExecutable(join(pathDir, "biome"));
|
|
186
|
+
process.env["PATH"] = pathDir;
|
|
187
|
+
|
|
188
|
+
// when / then
|
|
189
|
+
expect(resolveServerBinary(["biome"])).toBe(pathBinary);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("#given nothing anywhere #when resolving #then it returns null", () => {
|
|
193
|
+
// given
|
|
194
|
+
const root = makeTempRoot("lsp-unresolved-");
|
|
195
|
+
writeFileSync(join(root, "package.json"), "{}\n");
|
|
196
|
+
process.env["PATH"] = join(root, "definitely-empty");
|
|
197
|
+
|
|
198
|
+
// when / then
|
|
199
|
+
expect(resolveServerBinary(["biome"], root)).toBeNull();
|
|
200
|
+
expect(resolveServerBinary([], root)).toBeNull();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("resolveServerBinary containment", () => {
|
|
205
|
+
test("#given a marker only above the working directory root #when resolving with a stop boundary #then it does not escape the boundary", () => {
|
|
206
|
+
// given
|
|
207
|
+
const outer = makeTempRoot("lsp-boundary-outer-");
|
|
208
|
+
writeFileSync(join(outer, "package.json"), "{}\n");
|
|
209
|
+
writeExecutable(join(outer, "node_modules", ".bin", "biome"));
|
|
210
|
+
const inner = join(outer, "inner");
|
|
211
|
+
mkdirSync(inner, { recursive: true });
|
|
212
|
+
process.env["PATH"] = join(outer, "definitely-empty");
|
|
213
|
+
|
|
214
|
+
// when / then
|
|
215
|
+
expect(resolveServerBinary(["biome"], inner, { stopAt: inner })).toBeNull();
|
|
216
|
+
expect(resolveServerBinary(["biome"], inner)).toBe(join(outer, "node_modules", ".bin", "biome"));
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("#given a git repository boundary #when resolving #then the walk stops at the repository root", () => {
|
|
220
|
+
// given
|
|
221
|
+
const outer = makeTempRoot("lsp-git-boundary-");
|
|
222
|
+
writeFileSync(join(outer, "package.json"), "{}\n");
|
|
223
|
+
writeExecutable(join(outer, "node_modules", ".bin", "biome"));
|
|
224
|
+
const repo = join(outer, "repo");
|
|
225
|
+
mkdirSync(join(repo, ".git"), { recursive: true });
|
|
226
|
+
process.env["PATH"] = join(outer, "definitely-empty");
|
|
227
|
+
|
|
228
|
+
// when / then
|
|
229
|
+
expect(resolveServerBinary(["biome"], repo)).toBeNull();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("resolveServerBinary windows suffix probing", () => {
|
|
234
|
+
test("#given a windows platform and a .cmd shim #when resolving locally #then the suffixed binary is found", () => {
|
|
235
|
+
// given
|
|
236
|
+
const root = makeTempRoot("lsp-win-local-");
|
|
237
|
+
writeFileSync(join(root, "package.json"), "{}\n");
|
|
238
|
+
const binaryPath = writeExecutable(join(root, "node_modules", ".bin", "biome.cmd"));
|
|
239
|
+
process.env["PATH"] = join(root, "definitely-empty");
|
|
240
|
+
|
|
241
|
+
// when
|
|
242
|
+
const resolved = resolveServerBinary(["biome"], root, { platform: "win32", pathExt: ".COM;.EXE;.CMD" });
|
|
243
|
+
|
|
244
|
+
// then
|
|
245
|
+
expect(resolved).toBe(binaryPath);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("#given a windows platform and a PATH .exe #when resolving #then the suffixed PATH entry is found", () => {
|
|
249
|
+
// given
|
|
250
|
+
const pathDir = makeTempRoot("lsp-win-path-");
|
|
251
|
+
const binaryPath = writeExecutable(join(pathDir, "biome.exe"));
|
|
252
|
+
process.env["PATH"] = pathDir;
|
|
253
|
+
|
|
254
|
+
// when / then
|
|
255
|
+
expect(resolveServerBinary(["biome"], undefined, { platform: "win32", pathExt: ".COM;.EXE" })).toBe(binaryPath);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe("resolveServerBinary caching", () => {
|
|
260
|
+
test("#given a repeated lookup #when resolving twice #then the filesystem is probed only once", () => {
|
|
261
|
+
// given
|
|
262
|
+
const root = makeTempRoot("lsp-cache-hit-");
|
|
263
|
+
writeFileSync(join(root, "package.json"), "{}\n");
|
|
264
|
+
const binaryPath = writeExecutable(join(root, "node_modules", ".bin", "biome"));
|
|
265
|
+
process.env["PATH"] = join(root, "definitely-empty");
|
|
266
|
+
|
|
267
|
+
// when
|
|
268
|
+
const first = resolveServerBinary(["biome"], root);
|
|
269
|
+
const probesAfterFirst = __serverBinaryProbeCountForTests();
|
|
270
|
+
const second = resolveServerBinary(["biome"], root);
|
|
271
|
+
const probesAfterSecond = __serverBinaryProbeCountForTests();
|
|
272
|
+
|
|
273
|
+
// then
|
|
274
|
+
expect(first).toBe(binaryPath);
|
|
275
|
+
expect(second).toBe(binaryPath);
|
|
276
|
+
expect(probesAfterFirst).toBeGreaterThan(0);
|
|
277
|
+
expect(probesAfterSecond).toBe(probesAfterFirst);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("#given a cached negative lookup #when resolving twice #then the miss is also cached", () => {
|
|
281
|
+
// given
|
|
282
|
+
const root = makeTempRoot("lsp-cache-miss-");
|
|
283
|
+
writeFileSync(join(root, "package.json"), "{}\n");
|
|
284
|
+
process.env["PATH"] = join(root, "definitely-empty");
|
|
285
|
+
|
|
286
|
+
// when
|
|
287
|
+
expect(resolveServerBinary(["biome"], root)).toBeNull();
|
|
288
|
+
const probesAfterFirst = __serverBinaryProbeCountForTests();
|
|
289
|
+
expect(resolveServerBinary(["biome"], root)).toBeNull();
|
|
290
|
+
|
|
291
|
+
// then
|
|
292
|
+
expect(__serverBinaryProbeCountForTests()).toBe(probesAfterFirst);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("#given distinct working directories #when resolving the same command #then the cache keys stay separate", () => {
|
|
296
|
+
// given
|
|
297
|
+
const first = makeTempRoot("lsp-cache-key-a-");
|
|
298
|
+
writeFileSync(join(first, "package.json"), "{}\n");
|
|
299
|
+
const firstBinary = writeExecutable(join(first, "node_modules", ".bin", "biome"));
|
|
300
|
+
const second = makeTempRoot("lsp-cache-key-b-");
|
|
301
|
+
writeFileSync(join(second, "package.json"), "{}\n");
|
|
302
|
+
process.env["PATH"] = join(first, "definitely-empty");
|
|
303
|
+
|
|
304
|
+
// when / then
|
|
305
|
+
expect(resolveServerBinary(["biome"], first)).toBe(firstBinary);
|
|
306
|
+
expect(resolveServerBinary(["biome"], second)).toBeNull();
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe("isServerInstalled compatibility", () => {
|
|
311
|
+
test("#given a repo-local binary #when checking installation #then it reports installed", () => {
|
|
312
|
+
// given
|
|
313
|
+
const root = makeTempRoot("lsp-compat-local-");
|
|
314
|
+
writeFileSync(join(root, "package.json"), "{}\n");
|
|
315
|
+
writeExecutable(join(root, "node_modules", ".bin", "biome"));
|
|
316
|
+
process.env["PATH"] = join(root, "definitely-empty");
|
|
317
|
+
|
|
318
|
+
// when / then
|
|
319
|
+
expect(isServerInstalled(["biome", "lsp-proxy"], root)).toBe(true);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("#given a PATH binary and no working directory #when checking installation #then it reports installed", () => {
|
|
323
|
+
// given
|
|
324
|
+
const pathDir = makeTempRoot("lsp-compat-path-");
|
|
325
|
+
writeExecutable(join(pathDir, "typescript-language-server"));
|
|
326
|
+
process.env["PATH"] = pathDir;
|
|
327
|
+
|
|
328
|
+
// when / then
|
|
329
|
+
expect(isServerInstalled(["typescript-language-server", "--stdio"])).toBe(true);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("#given no binary at all #when checking installation #then it reports not installed", () => {
|
|
333
|
+
// given
|
|
334
|
+
const root = makeTempRoot("lsp-compat-missing-");
|
|
335
|
+
process.env["PATH"] = join(root, "definitely-empty");
|
|
336
|
+
|
|
337
|
+
// when / then
|
|
338
|
+
expect(isServerInstalled(["totally-absent-language-server"], root)).toBe(false);
|
|
339
|
+
expect(isServerInstalled([])).toBe(false);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("#given the node command #when checking installation #then the existing node allowance is preserved", () => {
|
|
343
|
+
// given
|
|
344
|
+
const root = makeTempRoot("lsp-compat-node-");
|
|
345
|
+
process.env["PATH"] = join(root, "definitely-empty");
|
|
346
|
+
|
|
347
|
+
// when / then
|
|
348
|
+
expect(isServerInstalled(["node", "server.js"], root)).toBe(true);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe("resolveServerBinary node fallback", () => {
|
|
353
|
+
test("#given a node command missing from PATH #when resolving #then the name is kept so the server stays selectable", () => {
|
|
354
|
+
// given: a user-configured server such as { command: ["node", "my-ls.js"] } must keep
|
|
355
|
+
// working, matching the node allowance isServerInstalled() has always had.
|
|
356
|
+
const root = makeTempRoot("lsp-node-fallback-");
|
|
357
|
+
process.env["PATH"] = join(root, "definitely-empty");
|
|
358
|
+
|
|
359
|
+
// when
|
|
360
|
+
const resolved = resolveServerBinary(["node", "server.js"], root);
|
|
361
|
+
|
|
362
|
+
// then: unresolved rather than rewritten, so the spawn still goes through node
|
|
363
|
+
expect(resolved).toBe("node");
|
|
364
|
+
expect(isServerInstalled(["node", "server.js"], root)).toBe(true);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test("#given a node command present on PATH #when resolving #then the PATH entry still wins", () => {
|
|
368
|
+
// given
|
|
369
|
+
const pathDir = makeTempRoot("lsp-node-on-path-");
|
|
370
|
+
const pathBinary = writeExecutable(join(pathDir, "node"));
|
|
371
|
+
process.env["PATH"] = pathDir;
|
|
372
|
+
|
|
373
|
+
// when / then
|
|
374
|
+
expect(resolveServerBinary(["node", "server.js"])).toBe(pathBinary);
|
|
375
|
+
});
|
|
376
|
+
});
|
|
@@ -1,45 +1,218 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { delimiter, join } from "node:path";
|
|
2
|
+
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Project markers that authorize probing a set of repo-local binary directories.
|
|
6
|
+
*
|
|
7
|
+
* A local bin directory is only trusted when the sibling marker proves the directory
|
|
8
|
+
* belongs to a project of that ecosystem. An unmarked `node_modules/.bin` is ignored so
|
|
9
|
+
* that a stray directory cannot inject an executable into the resolution order.
|
|
10
|
+
*/
|
|
11
|
+
interface LocalBinRule {
|
|
12
|
+
readonly markers: readonly string[];
|
|
13
|
+
readonly binDirs: readonly string[];
|
|
14
|
+
}
|
|
6
15
|
|
|
7
|
-
|
|
8
|
-
|
|
16
|
+
const LOCAL_BIN_RULES: readonly LocalBinRule[] = [
|
|
17
|
+
{
|
|
18
|
+
markers: ["package.json", "bun.lock", "bun.lockb", "package-lock.json", "yarn.lock", "pnpm-lock.yaml"],
|
|
19
|
+
binDirs: [join("node_modules", ".bin")],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
markers: [
|
|
23
|
+
"pyproject.toml",
|
|
24
|
+
"ty.toml",
|
|
25
|
+
"requirements.txt",
|
|
26
|
+
"setup.py",
|
|
27
|
+
"setup.cfg",
|
|
28
|
+
"Pipfile",
|
|
29
|
+
"pyrightconfig.json",
|
|
30
|
+
"ruff.toml",
|
|
31
|
+
".ruff.toml",
|
|
32
|
+
],
|
|
33
|
+
binDirs: [
|
|
34
|
+
join(".venv", "bin"),
|
|
35
|
+
join(".venv", "Scripts"),
|
|
36
|
+
join("venv", "bin"),
|
|
37
|
+
join("venv", "Scripts"),
|
|
38
|
+
join(".env", "bin"),
|
|
39
|
+
join(".env", "Scripts"),
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
markers: ["Gemfile", "Gemfile.lock"],
|
|
44
|
+
binDirs: [join("vendor", "bundle", "bin"), "bin"],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
markers: ["go.mod", "go.sum", "go.work"],
|
|
48
|
+
binDirs: ["bin"],
|
|
49
|
+
},
|
|
50
|
+
];
|
|
9
51
|
|
|
10
|
-
|
|
11
|
-
|
|
52
|
+
export interface ResolveServerBinaryOptions {
|
|
53
|
+
/**
|
|
54
|
+
* Upward walk boundary. The walk never ascends above this directory, which lets callers
|
|
55
|
+
* confine resolution to a trusted request cwd.
|
|
56
|
+
*/
|
|
57
|
+
readonly stopAt?: string;
|
|
58
|
+
readonly platform?: NodeJS.Platform;
|
|
59
|
+
readonly pathExt?: string;
|
|
60
|
+
readonly pathEnv?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const resolutionCache = new Map<string, string | null>();
|
|
64
|
+
let probeCount = 0;
|
|
65
|
+
|
|
66
|
+
/** Test seam: clears the per-process resolution cache. */
|
|
67
|
+
export function __resetServerBinaryResolutionCacheForTests(): void {
|
|
68
|
+
resolutionCache.clear();
|
|
69
|
+
probeCount = 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Test seam: counts filesystem probes so cache hits are observable. */
|
|
73
|
+
export function __serverBinaryProbeCountForTests(): number {
|
|
74
|
+
return probeCount;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function probe(path: string): boolean {
|
|
78
|
+
probeCount += 1;
|
|
79
|
+
return existsSync(path);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function executableSuffixes(platform: NodeJS.Platform, pathExt: string | undefined): readonly string[] {
|
|
83
|
+
if (platform !== "win32") return [""];
|
|
84
|
+
const configured = pathExt ?? process.env["PATHEXT"] ?? "";
|
|
85
|
+
// PATHEXT is conventionally uppercase, but the on-disk shims npm/bun write are lowercase.
|
|
86
|
+
// Probe lowercase first so the returned path matches the real filename on case-sensitive
|
|
87
|
+
// filesystems; the resolved path is spawned, so its casing has to be exact.
|
|
88
|
+
const systemExts = configured
|
|
89
|
+
.split(";")
|
|
90
|
+
.filter((entry) => entry.length > 0)
|
|
91
|
+
.map((entry) => entry.toLowerCase());
|
|
92
|
+
return [...new Set(["", ...systemExts, ".exe", ".cmd", ".bat", ".ps1"])];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function probeWithSuffixes(directory: string, command: string, suffixes: readonly string[]): string | null {
|
|
96
|
+
for (const suffix of suffixes) {
|
|
97
|
+
const candidate = join(directory, command + suffix);
|
|
98
|
+
if (probe(candidate)) return candidate;
|
|
12
99
|
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
13
102
|
|
|
14
|
-
|
|
103
|
+
function resolveLocal(
|
|
104
|
+
command: string,
|
|
105
|
+
workingDirectory: string,
|
|
106
|
+
suffixes: readonly string[],
|
|
107
|
+
stopAt: string | undefined,
|
|
108
|
+
): string | null {
|
|
109
|
+
const boundary = stopAt === undefined ? undefined : resolve(stopAt);
|
|
110
|
+
let current = resolve(workingDirectory);
|
|
15
111
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
exts = ["", ".exe", ".cmd", ".bat", ".ps1"];
|
|
112
|
+
while (true) {
|
|
113
|
+
for (const rule of LOCAL_BIN_RULES) {
|
|
114
|
+
if (!rule.markers.some((marker) => probe(join(current, marker)))) continue;
|
|
115
|
+
for (const binDir of rule.binDirs) {
|
|
116
|
+
const found = probeWithSuffixes(join(current, binDir), command, suffixes);
|
|
117
|
+
if (found !== null) return found;
|
|
118
|
+
}
|
|
24
119
|
}
|
|
120
|
+
|
|
121
|
+
// A repository root ends the walk: tooling above it belongs to a different project.
|
|
122
|
+
if (probe(join(current, ".git"))) return null;
|
|
123
|
+
if (boundary !== undefined && current === boundary) return null;
|
|
124
|
+
|
|
125
|
+
const parent = dirname(current);
|
|
126
|
+
if (parent === current) return null;
|
|
127
|
+
current = parent;
|
|
25
128
|
}
|
|
129
|
+
}
|
|
26
130
|
|
|
27
|
-
|
|
28
|
-
|
|
131
|
+
function resolveFromPath(
|
|
132
|
+
command: string,
|
|
133
|
+
suffixes: readonly string[],
|
|
134
|
+
platform: NodeJS.Platform,
|
|
135
|
+
pathEnvOverride: string | undefined,
|
|
136
|
+
): string | null {
|
|
137
|
+
let pathEnv = pathEnvOverride ?? process.env["PATH"] ?? "";
|
|
138
|
+
if (platform === "win32" && !pathEnv) {
|
|
29
139
|
pathEnv = process.env["Path"] ?? "";
|
|
30
140
|
}
|
|
31
141
|
|
|
32
|
-
const
|
|
142
|
+
for (const entry of pathEnv.split(delimiter)) {
|
|
143
|
+
if (entry.length === 0) continue;
|
|
144
|
+
const found = probeWithSuffixes(entry, command, suffixes);
|
|
145
|
+
if (found !== null) return found;
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
33
149
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Resolves the absolute path of an LSP server binary.
|
|
152
|
+
*
|
|
153
|
+
* Probing order:
|
|
154
|
+
* 1. a path-shaped command is validated as-is,
|
|
155
|
+
* 2. marker-gated repo-local bin directories, walking up from `workingDirectory`,
|
|
156
|
+
* 3. `PATH` (including Windows executable suffixes),
|
|
157
|
+
* 4. `null` when nothing matches.
|
|
158
|
+
*
|
|
159
|
+
* Results are cached per process, keyed by working directory and command.
|
|
160
|
+
*/
|
|
161
|
+
export function resolveServerBinary(
|
|
162
|
+
command: string[],
|
|
163
|
+
workingDirectory?: string,
|
|
164
|
+
options: ResolveServerBinaryOptions = {},
|
|
165
|
+
): string | null {
|
|
166
|
+
if (command.length === 0) return null;
|
|
167
|
+
const [cmd] = command;
|
|
168
|
+
if (cmd === undefined || cmd.length === 0) return null;
|
|
169
|
+
|
|
170
|
+
const platform = options.platform ?? process.platform;
|
|
171
|
+
const suffixes = executableSuffixes(platform, options.pathExt);
|
|
172
|
+
|
|
173
|
+
if (cmd.includes("/") || cmd.includes("\\")) {
|
|
174
|
+
const explicit = isAbsolute(cmd) ? cmd : resolve(workingDirectory ?? process.cwd(), cmd);
|
|
175
|
+
return probe(explicit) ? explicit : null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const cacheKey = JSON.stringify([
|
|
179
|
+
workingDirectory ?? "",
|
|
180
|
+
cmd,
|
|
181
|
+
platform,
|
|
182
|
+
options.stopAt ?? "",
|
|
183
|
+
options.pathExt ?? "",
|
|
184
|
+
options.pathEnv ?? process.env["PATH"] ?? "",
|
|
185
|
+
]);
|
|
186
|
+
const cached = resolutionCache.get(cacheKey);
|
|
187
|
+
if (cached !== undefined) return cached;
|
|
188
|
+
|
|
189
|
+
let resolved: string | null = null;
|
|
190
|
+
if (workingDirectory !== undefined && workingDirectory.length > 0) {
|
|
191
|
+
resolved = resolveLocal(cmd, workingDirectory, suffixes, options.stopAt);
|
|
40
192
|
}
|
|
193
|
+
resolved ??= resolveFromPath(cmd, suffixes, platform, options.pathEnv);
|
|
194
|
+
// A configured server may be launched through the interpreter itself, e.g.
|
|
195
|
+
// { command: ["node", "my-ls.js"] }. isServerInstalled() has always treated bare `node`
|
|
196
|
+
// as available regardless of PATH, so keep those servers selectable instead of
|
|
197
|
+
// regressing them into a not_installed result. The name is returned unresolved so the
|
|
198
|
+
// spawn still goes through `node`; rewriting it to process.execPath would silently
|
|
199
|
+
// substitute a different interpreter (bun) for the one the config asked for.
|
|
200
|
+
if (resolved === null && cmd === "node") resolved = cmd;
|
|
41
201
|
|
|
42
|
-
|
|
202
|
+
resolutionCache.set(cacheKey, resolved);
|
|
203
|
+
return resolved;
|
|
204
|
+
}
|
|
43
205
|
|
|
44
|
-
|
|
206
|
+
/**
|
|
207
|
+
* Reports whether an LSP server binary is available.
|
|
208
|
+
*
|
|
209
|
+
* Retained for existing consumers; `resolveServerBinary` is the richer entry point.
|
|
210
|
+
*/
|
|
211
|
+
export function isServerInstalled(
|
|
212
|
+
command: string[],
|
|
213
|
+
workingDirectory?: string,
|
|
214
|
+
options: ResolveServerBinaryOptions = {},
|
|
215
|
+
): boolean {
|
|
216
|
+
if (command.length === 0) return false;
|
|
217
|
+
return resolveServerBinary(command, workingDirectory, options) !== null;
|
|
45
218
|
}
|