@wavexzore/sandbox 0.1.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/Dockerfile +14 -0
- package/LICENSE +661 -0
- package/NOTICE +3 -0
- package/README.md +153 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/sandbox/cli/install.d.ts +5 -0
- package/dist/sandbox/cli/install.js +335 -0
- package/dist/sandbox/cli/local-store.d.ts +87 -0
- package/dist/sandbox/cli/local-store.js +604 -0
- package/dist/sandbox/cli/opencode-config.d.ts +25 -0
- package/dist/sandbox/cli/opencode-config.js +240 -0
- package/dist/sandbox/cli/path.d.ts +64 -0
- package/dist/sandbox/cli/path.js +127 -0
- package/dist/sandbox/cli/types.d.ts +145 -0
- package/dist/sandbox/cli/types.js +6 -0
- package/dist/sandbox/cli/wavexzore-sandbox.d.ts +65 -0
- package/dist/sandbox/cli/wavexzore-sandbox.js +577 -0
- package/dist/sandbox/core/cli-helper.d.ts +19 -0
- package/dist/sandbox/core/cli-helper.js +64 -0
- package/dist/sandbox/core/docker-archive-utils.d.ts +3 -0
- package/dist/sandbox/core/docker-archive-utils.js +50 -0
- package/dist/sandbox/core/docker-sandbox.d.ts +51 -0
- package/dist/sandbox/core/docker-sandbox.js +675 -0
- package/dist/sandbox/core/edit/filediff.d.ts +16 -0
- package/dist/sandbox/core/edit/filediff.js +21 -0
- package/dist/sandbox/core/edit/index.d.ts +5 -0
- package/dist/sandbox/core/edit/index.js +5 -0
- package/dist/sandbox/core/edit/line-endings.d.ts +4 -0
- package/dist/sandbox/core/edit/line-endings.js +10 -0
- package/dist/sandbox/core/edit/lock.d.ts +1 -0
- package/dist/sandbox/core/edit/lock.js +18 -0
- package/dist/sandbox/core/edit/replace.d.ts +10 -0
- package/dist/sandbox/core/edit/replace.js +14 -0
- package/dist/sandbox/core/edit/replacers.d.ts +15 -0
- package/dist/sandbox/core/edit/replacers.js +241 -0
- package/dist/sandbox/core/logger.d.ts +15 -0
- package/dist/sandbox/core/logger.js +59 -0
- package/dist/sandbox/core/lsp/client.d.ts +63 -0
- package/dist/sandbox/core/lsp/client.js +533 -0
- package/dist/sandbox/core/lsp/config.d.ts +13 -0
- package/dist/sandbox/core/lsp/config.js +36 -0
- package/dist/sandbox/core/lsp/diagnostics.d.ts +12 -0
- package/dist/sandbox/core/lsp/diagnostics.js +65 -0
- package/dist/sandbox/core/lsp/index.d.ts +4 -0
- package/dist/sandbox/core/lsp/index.js +4 -0
- package/dist/sandbox/core/lsp/language.d.ts +24 -0
- package/dist/sandbox/core/lsp/language.js +249 -0
- package/dist/sandbox/core/lsp/manager.d.ts +77 -0
- package/dist/sandbox/core/lsp/manager.js +237 -0
- package/dist/sandbox/core/lsp/tooling.d.ts +14 -0
- package/dist/sandbox/core/lsp/tooling.js +78 -0
- package/dist/sandbox/core/patch-parser.d.ts +23 -0
- package/dist/sandbox/core/patch-parser.js +248 -0
- package/dist/sandbox/core/path-map.d.ts +9 -0
- package/dist/sandbox/core/path-map.js +73 -0
- package/dist/sandbox/core/project-data-storage.d.ts +42 -0
- package/dist/sandbox/core/project-data-storage.js +167 -0
- package/dist/sandbox/core/read/binary.d.ts +4 -0
- package/dist/sandbox/core/read/binary.js +80 -0
- package/dist/sandbox/core/read/format.d.ts +38 -0
- package/dist/sandbox/core/read/format.js +85 -0
- package/dist/sandbox/core/read/index.d.ts +3 -0
- package/dist/sandbox/core/read/index.js +3 -0
- package/dist/sandbox/core/read/permissions.d.ts +7 -0
- package/dist/sandbox/core/read/permissions.js +13 -0
- package/dist/sandbox/core/session-manager.d.ts +29 -0
- package/dist/sandbox/core/session-manager.js +338 -0
- package/dist/sandbox/core/shell/config.d.ts +7 -0
- package/dist/sandbox/core/shell/config.js +82 -0
- package/dist/sandbox/core/shell/output.d.ts +35 -0
- package/dist/sandbox/core/shell/output.js +80 -0
- package/dist/sandbox/core/shell/parser.d.ts +7 -0
- package/dist/sandbox/core/shell/parser.js +122 -0
- package/dist/sandbox/core/shell/permissions.d.ts +13 -0
- package/dist/sandbox/core/shell/permissions.js +33 -0
- package/dist/sandbox/core/shell/workdir.d.ts +4 -0
- package/dist/sandbox/core/shell/workdir.js +19 -0
- package/dist/sandbox/core/stream-utils.d.ts +23 -0
- package/dist/sandbox/core/stream-utils.js +97 -0
- package/dist/sandbox/core/toast.d.ts +47 -0
- package/dist/sandbox/core/toast.js +73 -0
- package/dist/sandbox/core/types.d.ts +159 -0
- package/dist/sandbox/core/types.js +11 -0
- package/dist/sandbox/core/write/bom.d.ts +8 -0
- package/dist/sandbox/core/write/bom.js +15 -0
- package/dist/sandbox/core/write/config.d.ts +14 -0
- package/dist/sandbox/core/write/config.js +188 -0
- package/dist/sandbox/core/write/diagnostics.d.ts +19 -0
- package/dist/sandbox/core/write/diagnostics.js +120 -0
- package/dist/sandbox/core/write/diff.d.ts +7 -0
- package/dist/sandbox/core/write/diff.js +21 -0
- package/dist/sandbox/core/write/formatter.d.ts +16 -0
- package/dist/sandbox/core/write/formatter.js +51 -0
- package/dist/sandbox/core/write/index.d.ts +6 -0
- package/dist/sandbox/core/write/index.js +5 -0
- package/dist/sandbox/core/write/permissions.d.ts +13 -0
- package/dist/sandbox/core/write/permissions.js +19 -0
- package/dist/sandbox/core/write/pipeline.d.ts +48 -0
- package/dist/sandbox/core/write/pipeline.js +229 -0
- package/dist/sandbox/core/write/read-tracker.d.ts +13 -0
- package/dist/sandbox/core/write/read-tracker.js +30 -0
- package/dist/sandbox/git/host-git-manager.d.ts +40 -0
- package/dist/sandbox/git/host-git-manager.js +278 -0
- package/dist/sandbox/git/index.d.ts +5 -0
- package/dist/sandbox/git/index.js +5 -0
- package/dist/sandbox/git/sandbox-git-manager.d.ts +14 -0
- package/dist/sandbox/git/sandbox-git-manager.js +54 -0
- package/dist/sandbox/git/session-git-manager.d.ts +18 -0
- package/dist/sandbox/git/session-git-manager.js +85 -0
- package/dist/sandbox/index.d.ts +205 -0
- package/dist/sandbox/index.js +70 -0
- package/dist/sandbox/plugins/custom-tools.d.ts +203 -0
- package/dist/sandbox/plugins/custom-tools.js +15 -0
- package/dist/sandbox/plugins/session-events.d.ts +10 -0
- package/dist/sandbox/plugins/session-events.js +56 -0
- package/dist/sandbox/plugins/system-transform.d.ts +10 -0
- package/dist/sandbox/plugins/system-transform.js +23 -0
- package/dist/sandbox/tools/bash-output.d.ts +17 -0
- package/dist/sandbox/tools/bash-output.js +35 -0
- package/dist/sandbox/tools/bash-status.d.ts +13 -0
- package/dist/sandbox/tools/bash-status.js +29 -0
- package/dist/sandbox/tools/bash-stop.d.ts +13 -0
- package/dist/sandbox/tools/bash-stop.js +28 -0
- package/dist/sandbox/tools/bash.d.ts +26 -0
- package/dist/sandbox/tools/bash.js +120 -0
- package/dist/sandbox/tools/edit.d.ts +20 -0
- package/dist/sandbox/tools/edit.js +87 -0
- package/dist/sandbox/tools/get-preview-url.d.ts +17 -0
- package/dist/sandbox/tools/get-preview-url.js +16 -0
- package/dist/sandbox/tools/glob.d.ts +17 -0
- package/dist/sandbox/tools/glob.js +23 -0
- package/dist/sandbox/tools/grep.d.ts +17 -0
- package/dist/sandbox/tools/grep.js +23 -0
- package/dist/sandbox/tools/ls.d.ts +17 -0
- package/dist/sandbox/tools/ls.js +21 -0
- package/dist/sandbox/tools/lsp.d.ts +41 -0
- package/dist/sandbox/tools/lsp.js +198 -0
- package/dist/sandbox/tools/multiedit.d.ts +24 -0
- package/dist/sandbox/tools/multiedit.js +83 -0
- package/dist/sandbox/tools/patch.d.ts +14 -0
- package/dist/sandbox/tools/patch.js +260 -0
- package/dist/sandbox/tools/read.d.ts +22 -0
- package/dist/sandbox/tools/read.js +105 -0
- package/dist/sandbox/tools/write.d.ts +16 -0
- package/dist/sandbox/tools/write.js +27 -0
- package/dist/sandbox/tools.d.ts +200 -0
- package/dist/sandbox/tools.js +43 -0
- package/package.json +55 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { HostLspConfig, HostLspServerConfig } from './config.js';
|
|
2
|
+
export declare const LANGUAGE_EXTENSIONS: Record<string, string>;
|
|
3
|
+
export type HostLspCommand = {
|
|
4
|
+
command: string;
|
|
5
|
+
args: string[];
|
|
6
|
+
cwd: string;
|
|
7
|
+
env?: NodeJS.ProcessEnv;
|
|
8
|
+
initialization?: Record<string, unknown>;
|
|
9
|
+
};
|
|
10
|
+
export type HostLspServer = {
|
|
11
|
+
id: string;
|
|
12
|
+
extensions: string[];
|
|
13
|
+
rootMarkers: string[];
|
|
14
|
+
resolveCommand(root: string, worktree: string, config?: HostLspServerConfig): HostLspCommand | undefined;
|
|
15
|
+
};
|
|
16
|
+
export type ResolvedHostLspServer = {
|
|
17
|
+
id: string;
|
|
18
|
+
root: string;
|
|
19
|
+
command: HostLspCommand;
|
|
20
|
+
};
|
|
21
|
+
export declare function getLanguageId(filePath: string): string;
|
|
22
|
+
export declare function supportsHostLsp(filePath: string, config: HostLspConfig): boolean;
|
|
23
|
+
export declare function serverInstallHint(filePath: string): string;
|
|
24
|
+
export declare function resolveHostLspServers(filePath: string, worktree: string, config: HostLspConfig): Promise<ResolvedHostLspServer[]>;
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { accessSync, constants, existsSync } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { spawnSync } from 'child_process';
|
|
4
|
+
import { createRequire } from 'module';
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
export const LANGUAGE_EXTENSIONS = {
|
|
7
|
+
'.cjs': 'javascript',
|
|
8
|
+
'.cts': 'typescript',
|
|
9
|
+
'.go': 'go',
|
|
10
|
+
'.js': 'javascript',
|
|
11
|
+
'.jsx': 'javascriptreact',
|
|
12
|
+
'.mjs': 'javascript',
|
|
13
|
+
'.mts': 'typescript',
|
|
14
|
+
'.py': 'python',
|
|
15
|
+
'.pyi': 'python',
|
|
16
|
+
'.ts': 'typescript',
|
|
17
|
+
'.tsx': 'typescriptreact',
|
|
18
|
+
};
|
|
19
|
+
const TYPESCRIPT_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts'];
|
|
20
|
+
const GO_EXTENSIONS = ['.go'];
|
|
21
|
+
const PYTHON_EXTENSIONS = ['.py', '.pyi'];
|
|
22
|
+
const DEFAULT_SERVERS = [
|
|
23
|
+
{
|
|
24
|
+
id: 'typescript',
|
|
25
|
+
extensions: TYPESCRIPT_EXTENSIONS,
|
|
26
|
+
rootMarkers: ['tsconfig.json', 'jsconfig.json', 'package.json', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lock', 'bun.lockb'],
|
|
27
|
+
resolveCommand(root, worktree, config) {
|
|
28
|
+
const configured = commandFromConfig(root, worktree, config);
|
|
29
|
+
if (configured)
|
|
30
|
+
return configured;
|
|
31
|
+
const bin = findLocalBin('typescript-language-server', root, worktree) || findOnPath('typescript-language-server');
|
|
32
|
+
if (!bin)
|
|
33
|
+
return undefined;
|
|
34
|
+
const tsserver = resolveModuleFrom('typescript/lib/tsserver.js', [root, worktree]);
|
|
35
|
+
return {
|
|
36
|
+
command: bin,
|
|
37
|
+
args: ['--stdio'],
|
|
38
|
+
cwd: root,
|
|
39
|
+
env: process.env,
|
|
40
|
+
initialization: tsserver ? { tsserver: { path: tsserver } } : undefined,
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'go',
|
|
46
|
+
extensions: GO_EXTENSIONS,
|
|
47
|
+
rootMarkers: ['go.work', 'go.mod'],
|
|
48
|
+
resolveCommand(root, worktree, config) {
|
|
49
|
+
const configured = commandFromConfig(root, worktree, config);
|
|
50
|
+
if (configured)
|
|
51
|
+
return configured;
|
|
52
|
+
const bin = findOnPath('gopls');
|
|
53
|
+
if (!bin)
|
|
54
|
+
return undefined;
|
|
55
|
+
return { command: bin, args: [], cwd: root, env: process.env };
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'python',
|
|
60
|
+
extensions: PYTHON_EXTENSIONS,
|
|
61
|
+
rootMarkers: ['pyproject.toml', 'setup.py', 'setup.cfg', 'requirements.txt', 'Pipfile', 'poetry.lock'],
|
|
62
|
+
resolveCommand(root, worktree, config) {
|
|
63
|
+
const configured = commandFromConfig(root, worktree, config);
|
|
64
|
+
if (configured)
|
|
65
|
+
return configured;
|
|
66
|
+
const venvServer = findVenvBinary('pylsp', root, worktree);
|
|
67
|
+
if (venvServer)
|
|
68
|
+
return { command: venvServer, args: [], cwd: root, env: process.env };
|
|
69
|
+
const pylsp = findOnPath('pylsp');
|
|
70
|
+
if (pylsp)
|
|
71
|
+
return { command: pylsp, args: [], cwd: root, env: process.env };
|
|
72
|
+
const python = findOnPath('python3') || findOnPath('python');
|
|
73
|
+
if (python && hasPythonModule(python, 'pylsp')) {
|
|
74
|
+
return { command: python, args: ['-m', 'pylsp'], cwd: root, env: process.env };
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
export function getLanguageId(filePath) {
|
|
81
|
+
return LANGUAGE_EXTENSIONS[path.extname(filePath).toLowerCase()] ?? 'plaintext';
|
|
82
|
+
}
|
|
83
|
+
export function supportsHostLsp(filePath, config) {
|
|
84
|
+
if (!config.enabled)
|
|
85
|
+
return false;
|
|
86
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
87
|
+
return getServerDefinitions(config).some((server) => server.extensions.includes(ext));
|
|
88
|
+
}
|
|
89
|
+
export function serverInstallHint(filePath) {
|
|
90
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
91
|
+
if (TYPESCRIPT_EXTENSIONS.includes(ext)) {
|
|
92
|
+
return 'Install typescript and typescript-language-server in the host worktree, or configure .opencode/sandbox.json lsp.typescript.command.';
|
|
93
|
+
}
|
|
94
|
+
if (GO_EXTENSIONS.includes(ext)) {
|
|
95
|
+
return 'Install gopls on the host PATH, or configure .opencode/sandbox.json lsp.go.command.';
|
|
96
|
+
}
|
|
97
|
+
if (PYTHON_EXTENSIONS.includes(ext)) {
|
|
98
|
+
return 'Install python-lsp-server (pylsp) in the host venv/PATH, or configure .opencode/sandbox.json lsp.python.command.';
|
|
99
|
+
}
|
|
100
|
+
return `No host LSP server is configured for ${ext || 'this file type'}.`;
|
|
101
|
+
}
|
|
102
|
+
export async function resolveHostLspServers(filePath, worktree, config) {
|
|
103
|
+
if (!config.enabled)
|
|
104
|
+
return [];
|
|
105
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
106
|
+
const definitions = getServerDefinitions(config).filter((server) => server.extensions.includes(ext));
|
|
107
|
+
const results = [];
|
|
108
|
+
for (const server of definitions) {
|
|
109
|
+
const root = findNearestRoot(path.dirname(filePath), worktree, server.rootMarkers) ?? path.resolve(worktree);
|
|
110
|
+
const command = server.resolveCommand(root, worktree, config.servers[server.id]);
|
|
111
|
+
if (!command)
|
|
112
|
+
continue;
|
|
113
|
+
results.push({ id: server.id, root, command });
|
|
114
|
+
}
|
|
115
|
+
return results;
|
|
116
|
+
}
|
|
117
|
+
function getServerDefinitions(config) {
|
|
118
|
+
return DEFAULT_SERVERS.flatMap((server) => {
|
|
119
|
+
const override = config.servers[server.id];
|
|
120
|
+
if (override?.disabled)
|
|
121
|
+
return [];
|
|
122
|
+
return [
|
|
123
|
+
{
|
|
124
|
+
...server,
|
|
125
|
+
extensions: override?.extensions ?? server.extensions,
|
|
126
|
+
rootMarkers: override?.rootMarkers ?? server.rootMarkers,
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function findNearestRoot(startDir, stopDir, markers) {
|
|
132
|
+
let current = path.resolve(startDir);
|
|
133
|
+
const stop = path.resolve(stopDir);
|
|
134
|
+
while (true) {
|
|
135
|
+
if (markers.some((marker) => existsSync(path.join(current, marker))))
|
|
136
|
+
return current;
|
|
137
|
+
if (current === stop || !isInsideOrEqual(current, stop))
|
|
138
|
+
return undefined;
|
|
139
|
+
const parent = path.dirname(current);
|
|
140
|
+
if (parent === current)
|
|
141
|
+
return undefined;
|
|
142
|
+
current = parent;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function isInsideOrEqual(target, root) {
|
|
146
|
+
const relative = path.relative(path.resolve(root), path.resolve(target));
|
|
147
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
148
|
+
}
|
|
149
|
+
function commandFromConfig(root, worktree, config) {
|
|
150
|
+
if (!config?.command?.length)
|
|
151
|
+
return undefined;
|
|
152
|
+
const [rawCommand, ...args] = config.command;
|
|
153
|
+
return {
|
|
154
|
+
command: resolveConfiguredCommand(rawCommand, root, worktree),
|
|
155
|
+
args,
|
|
156
|
+
cwd: root,
|
|
157
|
+
env: { ...process.env, ...(config.env ?? {}) },
|
|
158
|
+
initialization: config.initialization,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function resolveConfiguredCommand(command, root, worktree) {
|
|
162
|
+
if (path.isAbsolute(command))
|
|
163
|
+
return command;
|
|
164
|
+
if (command.startsWith('.') || command.includes('/')) {
|
|
165
|
+
const rootCandidate = path.resolve(root, command);
|
|
166
|
+
if (existsSync(rootCandidate))
|
|
167
|
+
return rootCandidate;
|
|
168
|
+
return path.resolve(worktree, command);
|
|
169
|
+
}
|
|
170
|
+
return command;
|
|
171
|
+
}
|
|
172
|
+
function executableCandidates(name) {
|
|
173
|
+
if (process.platform === 'win32' && !path.extname(name))
|
|
174
|
+
return [name + '.cmd', name + '.exe', name];
|
|
175
|
+
return [name];
|
|
176
|
+
}
|
|
177
|
+
function canExecute(filePath) {
|
|
178
|
+
try {
|
|
179
|
+
accessSync(filePath, constants.X_OK);
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return existsSync(filePath);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function findOnPath(name) {
|
|
187
|
+
for (const dir of (process.env.PATH ?? '').split(path.delimiter)) {
|
|
188
|
+
if (!dir)
|
|
189
|
+
continue;
|
|
190
|
+
for (const candidateName of executableCandidates(name)) {
|
|
191
|
+
const candidate = path.join(dir, candidateName);
|
|
192
|
+
if (canExecute(candidate))
|
|
193
|
+
return candidate;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
function findLocalBin(name, root, worktree) {
|
|
199
|
+
let current = path.resolve(root);
|
|
200
|
+
const stop = path.resolve(worktree);
|
|
201
|
+
while (true) {
|
|
202
|
+
for (const candidateName of executableCandidates(name)) {
|
|
203
|
+
const candidate = path.join(current, 'node_modules', '.bin', candidateName);
|
|
204
|
+
if (canExecute(candidate))
|
|
205
|
+
return candidate;
|
|
206
|
+
}
|
|
207
|
+
if (current === stop || !isInsideOrEqual(current, stop))
|
|
208
|
+
return undefined;
|
|
209
|
+
const parent = path.dirname(current);
|
|
210
|
+
if (parent === current)
|
|
211
|
+
return undefined;
|
|
212
|
+
current = parent;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function findVenvBinary(name, root, worktree) {
|
|
216
|
+
let current = path.resolve(root);
|
|
217
|
+
const stop = path.resolve(worktree);
|
|
218
|
+
const binDir = process.platform === 'win32' ? 'Scripts' : 'bin';
|
|
219
|
+
while (true) {
|
|
220
|
+
for (const venvName of ['.venv', 'venv']) {
|
|
221
|
+
for (const candidateName of executableCandidates(name)) {
|
|
222
|
+
const candidate = path.join(current, venvName, binDir, candidateName);
|
|
223
|
+
if (canExecute(candidate))
|
|
224
|
+
return candidate;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (current === stop || !isInsideOrEqual(current, stop))
|
|
228
|
+
return undefined;
|
|
229
|
+
const parent = path.dirname(current);
|
|
230
|
+
if (parent === current)
|
|
231
|
+
return undefined;
|
|
232
|
+
current = parent;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function resolveModuleFrom(moduleName, roots) {
|
|
236
|
+
for (const root of roots) {
|
|
237
|
+
try {
|
|
238
|
+
return require.resolve(moduleName, { paths: [root] });
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
void error;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
function hasPythonModule(python, moduleName) {
|
|
247
|
+
const result = spawnSync(python, ['-c', `import ${moduleName}`], { stdio: 'ignore', timeout: 3000 });
|
|
248
|
+
return result.status === 0;
|
|
249
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Diagnostic, Position } from 'vscode-languageserver-types';
|
|
2
|
+
export declare class HostLspUnavailableError extends Error {
|
|
3
|
+
readonly filePath: string;
|
|
4
|
+
readonly reason: string;
|
|
5
|
+
constructor(filePath: string, reason: string);
|
|
6
|
+
}
|
|
7
|
+
type DiagnosticMode = 'document' | 'full';
|
|
8
|
+
type LocationInput = {
|
|
9
|
+
sessionID: string;
|
|
10
|
+
worktree: string;
|
|
11
|
+
filePath: string;
|
|
12
|
+
position: Position;
|
|
13
|
+
};
|
|
14
|
+
export declare class HostLspManager {
|
|
15
|
+
private readonly clients;
|
|
16
|
+
private readonly spawning;
|
|
17
|
+
private readonly broken;
|
|
18
|
+
touchFile(input: {
|
|
19
|
+
sessionID: string;
|
|
20
|
+
worktree: string;
|
|
21
|
+
filePath: string;
|
|
22
|
+
diagnostics?: DiagnosticMode;
|
|
23
|
+
}): Promise<number>;
|
|
24
|
+
closeFile(input: {
|
|
25
|
+
sessionID: string;
|
|
26
|
+
worktree: string;
|
|
27
|
+
filePath: string;
|
|
28
|
+
}): Promise<void>;
|
|
29
|
+
diagnosticsForFile(input: {
|
|
30
|
+
sessionID: string;
|
|
31
|
+
worktree: string;
|
|
32
|
+
filePath: string;
|
|
33
|
+
}): Diagnostic[];
|
|
34
|
+
allDiagnostics(input?: {
|
|
35
|
+
sessionID?: string;
|
|
36
|
+
worktree?: string;
|
|
37
|
+
}): Map<string, Diagnostic[]>;
|
|
38
|
+
hover(input: LocationInput): Promise<unknown>;
|
|
39
|
+
definition(input: LocationInput): Promise<any[]>;
|
|
40
|
+
references(input: LocationInput): Promise<any[]>;
|
|
41
|
+
implementation(input: LocationInput): Promise<any[]>;
|
|
42
|
+
prepareCallHierarchy(input: LocationInput): Promise<unknown[]>;
|
|
43
|
+
incomingCalls(input: LocationInput): Promise<unknown[]>;
|
|
44
|
+
outgoingCalls(input: LocationInput): Promise<unknown[]>;
|
|
45
|
+
documentSymbol(input: {
|
|
46
|
+
sessionID: string;
|
|
47
|
+
worktree: string;
|
|
48
|
+
filePath: string;
|
|
49
|
+
}): Promise<unknown[]>;
|
|
50
|
+
workspaceSymbol(input: {
|
|
51
|
+
sessionID: string;
|
|
52
|
+
worktree: string;
|
|
53
|
+
query: string;
|
|
54
|
+
}): Promise<unknown[]>;
|
|
55
|
+
status(input?: {
|
|
56
|
+
sessionID?: string;
|
|
57
|
+
}): Promise<{
|
|
58
|
+
connected: any[];
|
|
59
|
+
broken: {
|
|
60
|
+
key: string;
|
|
61
|
+
reason: string;
|
|
62
|
+
}[];
|
|
63
|
+
}>;
|
|
64
|
+
hasClients(input: {
|
|
65
|
+
sessionID: string;
|
|
66
|
+
worktree: string;
|
|
67
|
+
filePath: string;
|
|
68
|
+
}): Promise<boolean>;
|
|
69
|
+
shutdownSession(sessionID: string): Promise<void>;
|
|
70
|
+
shutdownAll(): Promise<void>;
|
|
71
|
+
private runForFile;
|
|
72
|
+
private getClients;
|
|
73
|
+
private getExistingClientsForFile;
|
|
74
|
+
private spawnClient;
|
|
75
|
+
}
|
|
76
|
+
export declare const hostLspManager: HostLspManager;
|
|
77
|
+
export {};
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { HostLspClient } from './client.js';
|
|
4
|
+
import { loadHostLspConfig } from './config.js';
|
|
5
|
+
import { resolveHostLspServers, serverInstallHint, supportsHostLsp } from './language.js';
|
|
6
|
+
import { logger } from '../logger.js';
|
|
7
|
+
export class HostLspUnavailableError extends Error {
|
|
8
|
+
filePath;
|
|
9
|
+
reason;
|
|
10
|
+
constructor(filePath, reason) {
|
|
11
|
+
super(reason);
|
|
12
|
+
this.filePath = filePath;
|
|
13
|
+
this.reason = reason;
|
|
14
|
+
this.name = 'HostLspUnavailableError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function keyFor(sessionID, serverID, root) {
|
|
18
|
+
return `${sessionID}\0${serverID}\0${root}`;
|
|
19
|
+
}
|
|
20
|
+
function normalizeHostFile(filePath) {
|
|
21
|
+
return path.resolve(filePath);
|
|
22
|
+
}
|
|
23
|
+
function isInsideOrEqual(target, root) {
|
|
24
|
+
const relative = path.relative(path.resolve(root), path.resolve(target));
|
|
25
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
26
|
+
}
|
|
27
|
+
function flattenResults(items) {
|
|
28
|
+
return items.flatMap((item) => (Array.isArray(item) ? item : item ? [item] : []));
|
|
29
|
+
}
|
|
30
|
+
export class HostLspManager {
|
|
31
|
+
clients = new Map();
|
|
32
|
+
spawning = new Map();
|
|
33
|
+
broken = new Map();
|
|
34
|
+
async touchFile(input) {
|
|
35
|
+
const filePath = normalizeHostFile(input.filePath);
|
|
36
|
+
const clients = await this.getClients(input.sessionID, input.worktree, filePath);
|
|
37
|
+
await Promise.all(clients.map((record) => record.client.touchFile(filePath, input.diagnostics)));
|
|
38
|
+
return clients.length;
|
|
39
|
+
}
|
|
40
|
+
async closeFile(input) {
|
|
41
|
+
const filePath = normalizeHostFile(input.filePath);
|
|
42
|
+
const clients = await this.getExistingClientsForFile(input.sessionID, input.worktree, filePath);
|
|
43
|
+
await Promise.all(clients.map((record) => record.client.closeFile(filePath)));
|
|
44
|
+
}
|
|
45
|
+
diagnosticsForFile(input) {
|
|
46
|
+
const filePath = normalizeHostFile(input.filePath);
|
|
47
|
+
const diagnostics = [];
|
|
48
|
+
for (const record of this.clients.values()) {
|
|
49
|
+
if (record.sessionID !== input.sessionID)
|
|
50
|
+
continue;
|
|
51
|
+
if (!isInsideOrEqual(filePath, record.root))
|
|
52
|
+
continue;
|
|
53
|
+
diagnostics.push(...(record.client.diagnostics.get(filePath) ?? []));
|
|
54
|
+
}
|
|
55
|
+
return diagnostics;
|
|
56
|
+
}
|
|
57
|
+
allDiagnostics(input) {
|
|
58
|
+
const result = new Map();
|
|
59
|
+
for (const record of this.clients.values()) {
|
|
60
|
+
if (input?.sessionID && record.sessionID !== input.sessionID)
|
|
61
|
+
continue;
|
|
62
|
+
if (input?.worktree && path.resolve(record.worktree) !== path.resolve(input.worktree))
|
|
63
|
+
continue;
|
|
64
|
+
for (const [filePath, diagnostics] of record.client.diagnostics.entries()) {
|
|
65
|
+
result.set(filePath, [...(result.get(filePath) ?? []), ...diagnostics]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
async hover(input) {
|
|
71
|
+
await this.touchFile({ ...input, diagnostics: 'document' });
|
|
72
|
+
const results = await this.runForFile(input, (client) => client.hover({ filePath: input.filePath, position: input.position }));
|
|
73
|
+
return results.find(Boolean) ?? null;
|
|
74
|
+
}
|
|
75
|
+
async definition(input) {
|
|
76
|
+
await this.touchFile({ ...input, diagnostics: 'document' });
|
|
77
|
+
const results = await this.runForFile(input, (client) => client.definition({ filePath: input.filePath, position: input.position }).catch(() => null));
|
|
78
|
+
return flattenResults(results);
|
|
79
|
+
}
|
|
80
|
+
async references(input) {
|
|
81
|
+
await this.touchFile({ ...input, diagnostics: 'document' });
|
|
82
|
+
const results = await this.runForFile(input, (client) => client.references({ filePath: input.filePath, position: input.position }).catch(() => []));
|
|
83
|
+
return flattenResults(results);
|
|
84
|
+
}
|
|
85
|
+
async implementation(input) {
|
|
86
|
+
await this.touchFile({ ...input, diagnostics: 'document' });
|
|
87
|
+
const results = await this.runForFile(input, (client) => client.implementation({ filePath: input.filePath, position: input.position }).catch(() => null));
|
|
88
|
+
return flattenResults(results);
|
|
89
|
+
}
|
|
90
|
+
async prepareCallHierarchy(input) {
|
|
91
|
+
await this.touchFile({ ...input, diagnostics: 'document' });
|
|
92
|
+
const results = await this.runForFile(input, (client) => client.prepareCallHierarchy({ filePath: input.filePath, position: input.position }).catch(() => []));
|
|
93
|
+
return flattenResults(results);
|
|
94
|
+
}
|
|
95
|
+
async incomingCalls(input) {
|
|
96
|
+
await this.touchFile({ ...input, diagnostics: 'document' });
|
|
97
|
+
const results = await this.runForFile(input, async (client) => {
|
|
98
|
+
const items = (await client.prepareCallHierarchy({ filePath: input.filePath, position: input.position }).catch(() => [])) ?? [];
|
|
99
|
+
const calls = await Promise.all(items.map((item) => client.incomingCalls(item).catch(() => [])));
|
|
100
|
+
return flattenResults(calls);
|
|
101
|
+
});
|
|
102
|
+
return flattenResults(results);
|
|
103
|
+
}
|
|
104
|
+
async outgoingCalls(input) {
|
|
105
|
+
await this.touchFile({ ...input, diagnostics: 'document' });
|
|
106
|
+
const results = await this.runForFile(input, async (client) => {
|
|
107
|
+
const items = (await client.prepareCallHierarchy({ filePath: input.filePath, position: input.position }).catch(() => [])) ?? [];
|
|
108
|
+
const calls = await Promise.all(items.map((item) => client.outgoingCalls(item).catch(() => [])));
|
|
109
|
+
return flattenResults(calls);
|
|
110
|
+
});
|
|
111
|
+
return flattenResults(results);
|
|
112
|
+
}
|
|
113
|
+
async documentSymbol(input) {
|
|
114
|
+
await this.touchFile({ ...input, diagnostics: 'document' });
|
|
115
|
+
const results = await this.runForFile(input, (client) => client.documentSymbol(input.filePath).catch(() => []));
|
|
116
|
+
return flattenResults(results);
|
|
117
|
+
}
|
|
118
|
+
async workspaceSymbol(input) {
|
|
119
|
+
const records = [...this.clients.values()].filter((record) => record.sessionID === input.sessionID && path.resolve(record.worktree) === path.resolve(input.worktree));
|
|
120
|
+
const results = await Promise.all(records.map((record) => record.client.workspaceSymbol(input.query).catch(() => [])));
|
|
121
|
+
return flattenResults(results);
|
|
122
|
+
}
|
|
123
|
+
async status(input) {
|
|
124
|
+
const connected = [];
|
|
125
|
+
for (const record of this.clients.values()) {
|
|
126
|
+
if (input?.sessionID && record.sessionID !== input.sessionID)
|
|
127
|
+
continue;
|
|
128
|
+
connected.push({ ...(await record.client.status()), worktree: record.worktree });
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
connected,
|
|
132
|
+
broken: [...this.broken.entries()].map(([key, reason]) => ({ key, reason })),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async hasClients(input) {
|
|
136
|
+
try {
|
|
137
|
+
return (await this.getClients(input.sessionID, input.worktree, normalizeHostFile(input.filePath))).length > 0;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async shutdownSession(sessionID) {
|
|
144
|
+
const records = [...this.clients.entries()].filter(([, record]) => record.sessionID === sessionID);
|
|
145
|
+
for (const [key, record] of records) {
|
|
146
|
+
this.clients.delete(key);
|
|
147
|
+
await record.client.shutdown().catch((error) => logger.warn(`[lsp] failed to shutdown ${record.serverID}: ${error}`));
|
|
148
|
+
}
|
|
149
|
+
for (const key of [...this.broken.keys()]) {
|
|
150
|
+
if (key.startsWith(`${sessionID}\0`))
|
|
151
|
+
this.broken.delete(key);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async shutdownAll() {
|
|
155
|
+
await Promise.all([...this.clients.values()].map((record) => record.client.shutdown().catch(() => undefined)));
|
|
156
|
+
this.clients.clear();
|
|
157
|
+
this.spawning.clear();
|
|
158
|
+
this.broken.clear();
|
|
159
|
+
}
|
|
160
|
+
async runForFile(input, fn) {
|
|
161
|
+
const filePath = normalizeHostFile(input.filePath);
|
|
162
|
+
const clients = await this.getClients(input.sessionID, input.worktree, filePath);
|
|
163
|
+
if (clients.length === 0) {
|
|
164
|
+
throw new HostLspUnavailableError(filePath, `No host LSP server available for ${path.extname(filePath) || 'this file'}. ${serverInstallHint(filePath)}`);
|
|
165
|
+
}
|
|
166
|
+
return Promise.all(clients.map((record) => fn(record.client)));
|
|
167
|
+
}
|
|
168
|
+
async getClients(sessionID, worktree, filePath) {
|
|
169
|
+
const hostWorktree = path.resolve(worktree);
|
|
170
|
+
if (!isInsideOrEqual(filePath, hostWorktree))
|
|
171
|
+
return [];
|
|
172
|
+
if (!existsSync(filePath)) {
|
|
173
|
+
throw new HostLspUnavailableError(filePath, `Host file not found for LSP validation: ${filePath}`);
|
|
174
|
+
}
|
|
175
|
+
const config = loadHostLspConfig(hostWorktree);
|
|
176
|
+
if (!supportsHostLsp(filePath, config))
|
|
177
|
+
return [];
|
|
178
|
+
const servers = await resolveHostLspServers(filePath, hostWorktree, config);
|
|
179
|
+
if (servers.length === 0) {
|
|
180
|
+
throw new HostLspUnavailableError(filePath, `No host LSP server available for ${path.extname(filePath) || 'this file'}. ${serverInstallHint(filePath)}`);
|
|
181
|
+
}
|
|
182
|
+
const records = [];
|
|
183
|
+
const attemptedKeys = [];
|
|
184
|
+
for (const server of servers) {
|
|
185
|
+
const key = keyFor(sessionID, server.id, server.root);
|
|
186
|
+
attemptedKeys.push(key);
|
|
187
|
+
const brokenReason = this.broken.get(key);
|
|
188
|
+
if (brokenReason)
|
|
189
|
+
throw new HostLspUnavailableError(filePath, brokenReason);
|
|
190
|
+
const existing = this.clients.get(key);
|
|
191
|
+
if (existing) {
|
|
192
|
+
records.push(existing);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const inflight = this.spawning.get(key);
|
|
196
|
+
if (inflight) {
|
|
197
|
+
const record = await inflight;
|
|
198
|
+
if (record)
|
|
199
|
+
records.push(record);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const task = this.spawnClient(sessionID, hostWorktree, server.id, server.root, server.command, key);
|
|
203
|
+
this.spawning.set(key, task);
|
|
204
|
+
task.finally(() => {
|
|
205
|
+
if (this.spawning.get(key) === task)
|
|
206
|
+
this.spawning.delete(key);
|
|
207
|
+
});
|
|
208
|
+
const record = await task;
|
|
209
|
+
if (record)
|
|
210
|
+
records.push(record);
|
|
211
|
+
}
|
|
212
|
+
if (records.length === 0) {
|
|
213
|
+
const reason = attemptedKeys.map((key) => this.broken.get(key)).find(Boolean);
|
|
214
|
+
throw new HostLspUnavailableError(filePath, reason ?? `No host LSP server available for ${path.extname(filePath) || 'this file'}. ${serverInstallHint(filePath)}`);
|
|
215
|
+
}
|
|
216
|
+
return records;
|
|
217
|
+
}
|
|
218
|
+
async getExistingClientsForFile(sessionID, worktree, filePath) {
|
|
219
|
+
const hostWorktree = path.resolve(worktree);
|
|
220
|
+
return [...this.clients.values()].filter((record) => record.sessionID === sessionID && path.resolve(record.worktree) === hostWorktree && isInsideOrEqual(filePath, record.root));
|
|
221
|
+
}
|
|
222
|
+
async spawnClient(sessionID, worktree, serverID, root, command, key) {
|
|
223
|
+
try {
|
|
224
|
+
const client = await HostLspClient.create({ serverID, root, command });
|
|
225
|
+
const record = { sessionID, worktree, serverID, root, client };
|
|
226
|
+
this.clients.set(key, record);
|
|
227
|
+
return record;
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
const reason = `Failed to start host LSP server ${serverID} at ${root}: ${error?.message || error}`;
|
|
231
|
+
logger.error(`[lsp] ${reason}`);
|
|
232
|
+
this.broken.set(key, reason);
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
export const hostLspManager = new HostLspManager();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PathMapper } from '../path-map.js';
|
|
2
|
+
type NotifyInput = {
|
|
3
|
+
sessionID: string;
|
|
4
|
+
worktree: string;
|
|
5
|
+
paths: PathMapper;
|
|
6
|
+
inputPath: string;
|
|
7
|
+
containerPath?: string;
|
|
8
|
+
expectedContent?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function waitForHostFile(hostPath: string, expectedContent?: string): Promise<void>;
|
|
11
|
+
export declare function notifyHostLspAfterWrite(input: NotifyInput): Promise<string>;
|
|
12
|
+
export declare function notifyHostLspAfterDelete(input: Omit<NotifyInput, 'expectedContent'>): Promise<string>;
|
|
13
|
+
export declare function warmHostLspFile(input: Omit<NotifyInput, 'expectedContent'>): void;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { readFile } from 'fs/promises';
|
|
3
|
+
import { logger } from '../logger.js';
|
|
4
|
+
import { hostLspManager } from './manager.js';
|
|
5
|
+
import { formatLspDiagnosticOutput, formatLspUnavailable, hasActionableDiagnostics } from './diagnostics.js';
|
|
6
|
+
const HOST_SYNC_TIMEOUT_MS = 5000;
|
|
7
|
+
const HOST_SYNC_INTERVAL_MS = 50;
|
|
8
|
+
function delay(ms) {
|
|
9
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
|
+
}
|
|
11
|
+
export async function waitForHostFile(hostPath, expectedContent) {
|
|
12
|
+
const startedAt = Date.now();
|
|
13
|
+
let lastContent;
|
|
14
|
+
while (Date.now() - startedAt <= HOST_SYNC_TIMEOUT_MS) {
|
|
15
|
+
if (existsSync(hostPath)) {
|
|
16
|
+
if (expectedContent === undefined)
|
|
17
|
+
return;
|
|
18
|
+
try {
|
|
19
|
+
lastContent = await readFile(hostPath, 'utf8');
|
|
20
|
+
if (lastContent === expectedContent)
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
void error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
await delay(HOST_SYNC_INTERVAL_MS);
|
|
28
|
+
}
|
|
29
|
+
if (expectedContent === undefined) {
|
|
30
|
+
throw new Error(`Timed out waiting for host file ${hostPath}`);
|
|
31
|
+
}
|
|
32
|
+
throw new Error(`Timed out waiting for host file ${hostPath} to match sandbox write (${lastContent?.length ?? 0}/${expectedContent.length} bytes visible)`);
|
|
33
|
+
}
|
|
34
|
+
export async function notifyHostLspAfterWrite(input) {
|
|
35
|
+
const hostPath = input.paths.toHost(input.containerPath ?? input.inputPath);
|
|
36
|
+
if (!input.paths.isMapped(hostPath))
|
|
37
|
+
return undefined;
|
|
38
|
+
try {
|
|
39
|
+
await waitForHostFile(hostPath, input.expectedContent);
|
|
40
|
+
await hostLspManager.touchFile({
|
|
41
|
+
sessionID: input.sessionID,
|
|
42
|
+
worktree: input.worktree,
|
|
43
|
+
filePath: hostPath,
|
|
44
|
+
diagnostics: 'document',
|
|
45
|
+
});
|
|
46
|
+
const diagnostics = hostLspManager.diagnosticsForFile({
|
|
47
|
+
sessionID: input.sessionID,
|
|
48
|
+
worktree: input.worktree,
|
|
49
|
+
filePath: hostPath,
|
|
50
|
+
});
|
|
51
|
+
if (!hasActionableDiagnostics(diagnostics))
|
|
52
|
+
return undefined;
|
|
53
|
+
return formatLspDiagnosticOutput(input.paths.toRelative(hostPath), diagnostics);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
return formatLspUnavailable(error);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export async function notifyHostLspAfterDelete(input) {
|
|
60
|
+
const hostPath = input.paths.toHost(input.containerPath ?? input.inputPath);
|
|
61
|
+
if (!input.paths.isMapped(hostPath))
|
|
62
|
+
return undefined;
|
|
63
|
+
try {
|
|
64
|
+
await hostLspManager.closeFile({ sessionID: input.sessionID, worktree: input.worktree, filePath: hostPath });
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
return formatLspUnavailable(error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function warmHostLspFile(input) {
|
|
72
|
+
const hostPath = input.paths.toHost(input.containerPath ?? input.inputPath);
|
|
73
|
+
if (!input.paths.isMapped(hostPath))
|
|
74
|
+
return;
|
|
75
|
+
void hostLspManager
|
|
76
|
+
.touchFile({ sessionID: input.sessionID, worktree: input.worktree, filePath: hostPath })
|
|
77
|
+
.catch((error) => logger.warn(`[lsp] warm skipped for ${hostPath}: ${error?.message || error}`));
|
|
78
|
+
}
|