@octocodeai/octocode-engine 16.5.1 → 16.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +153 -235
- package/dist/lsp/client.d.ts +6 -0
- package/dist/lsp/client.js +31 -0
- package/dist/lsp/config.d.ts +29 -1
- package/dist/lsp/config.js +156 -1
- package/dist/lsp/ideContext.d.ts +18 -0
- package/dist/lsp/ideContext.js +29 -0
- package/dist/lsp/index.d.ts +8 -3
- package/dist/lsp/index.js +7 -2
- package/dist/lsp/manager.d.ts +11 -0
- package/dist/lsp/manager.js +75 -13
- package/dist/lsp/native.d.ts +5 -0
- package/dist/lsp/platform.d.ts +20 -0
- package/dist/lsp/platform.js +64 -0
- package/dist/lsp/serverDiscovery.d.ts +63 -0
- package/dist/lsp/serverDiscovery.js +226 -0
- package/dist/lsp/serverManifest.d.ts +70 -0
- package/dist/lsp/serverManifest.js +78 -0
- package/dist/lsp/serverManifestData.d.ts +2 -0
- package/dist/lsp/serverManifestData.js +33 -0
- package/dist/lsp/serverProvisioner.d.ts +19 -0
- package/dist/lsp/serverProvisioner.js +253 -0
- package/dist/lsp/types.d.ts +7 -0
- package/dist/lsp/workspaceRoot.d.ts +1 -1
- package/dist/lsp/workspaceRoot.js +3 -1
- package/dist/security/commandValidator.js +10 -10
- package/dist/security/discoveryFilter.js +7 -3
- package/dist/security/filePatterns.js +6 -0
- package/dist/security/ignoredPathFilter.js +5 -0
- package/dist/security/mask.js +5 -22
- package/dist/security/maskUtils.d.ts +6 -0
- package/dist/security/maskUtils.js +22 -0
- package/dist/security/native.js +5 -22
- package/dist/security/pathPatterns.js +5 -0
- package/dist/security/pathValidator.js +21 -18
- package/dist/security/withSecurityValidation.d.ts +0 -3
- package/dist/security/withSecurityValidation.js +3 -21
- package/index.cjs +32 -2
- package/index.d.ts +113 -0
- package/index.js +5 -0
- package/package.json +49 -8
package/dist/lsp/client.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CallHierarchyItem, CodeSnippet, ExactPosition, IncomingCall, LanguageServerConfig, OutgoingCall } from './types.js';
|
|
2
2
|
export declare class LSPClient {
|
|
3
3
|
private readonly nativeClient;
|
|
4
|
+
private readonly command;
|
|
4
5
|
private initialized;
|
|
5
6
|
constructor(config: LanguageServerConfig);
|
|
6
7
|
start(): Promise<void>;
|
|
@@ -20,6 +21,11 @@ export declare class LSPClient {
|
|
|
20
21
|
prepareCallHierarchy(filePath: string, position: ExactPosition, content?: string): Promise<CallHierarchyItem[]>;
|
|
21
22
|
getIncomingCalls(item: CallHierarchyItem): Promise<IncomingCall[]>;
|
|
22
23
|
getOutgoingCalls(item: CallHierarchyItem): Promise<OutgoingCall[]>;
|
|
24
|
+
workspaceSymbol(query: string): Promise<unknown[]>;
|
|
25
|
+
prepareTypeHierarchy(filePath: string, position: ExactPosition, content?: string): Promise<unknown[]>;
|
|
26
|
+
typeHierarchySupertypes(item: unknown): Promise<unknown[]>;
|
|
27
|
+
typeHierarchySubtypes(item: unknown): Promise<unknown[]>;
|
|
28
|
+
getDiagnostics(filePath: string, content?: string): Promise<unknown>;
|
|
23
29
|
hasCapability(_capability: string): boolean;
|
|
24
30
|
getRecentStderr(): string[];
|
|
25
31
|
openDocument(filePath: string, content?: string): Promise<void>;
|
package/dist/lsp/client.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import { nativeBinding } from './native.js';
|
|
3
|
+
import { validateLSPServerPath } from './validation.js';
|
|
3
4
|
export class LSPClient {
|
|
4
5
|
nativeClient;
|
|
6
|
+
command;
|
|
5
7
|
initialized = false;
|
|
6
8
|
constructor(config) {
|
|
9
|
+
this.command = config.command;
|
|
7
10
|
this.nativeClient = new nativeBinding.NativeLspClient({
|
|
8
11
|
command: config.command,
|
|
9
12
|
args: config.args,
|
|
@@ -14,6 +17,13 @@ export class LSPClient {
|
|
|
14
17
|
});
|
|
15
18
|
}
|
|
16
19
|
async start() {
|
|
20
|
+
// Security gate: never spawn a command that isn't a real, executable,
|
|
21
|
+
// non-shell server binary — even one resolved from the managed download
|
|
22
|
+
// cache. This is the single chokepoint before the native process spawn.
|
|
23
|
+
const validation = validateLSPServerPath(this.command);
|
|
24
|
+
if (!validation.isValid) {
|
|
25
|
+
throw new Error(`Refusing to start language server: ${validation.error ?? `invalid server path '${this.command}'`}`);
|
|
26
|
+
}
|
|
17
27
|
await this.nativeClient.start();
|
|
18
28
|
this.initialized = true;
|
|
19
29
|
}
|
|
@@ -76,6 +86,27 @@ export class LSPClient {
|
|
|
76
86
|
const result = await this.nativeClient.outgoingCalls(item);
|
|
77
87
|
return Array.isArray(result) ? result : [];
|
|
78
88
|
}
|
|
89
|
+
async workspaceSymbol(query) {
|
|
90
|
+
const result = await this.nativeClient.workspaceSymbol(query);
|
|
91
|
+
return Array.isArray(result) ? result : [];
|
|
92
|
+
}
|
|
93
|
+
async prepareTypeHierarchy(filePath, position, content) {
|
|
94
|
+
await this.openDocument(filePath, content);
|
|
95
|
+
const result = await this.nativeClient.prepareTypeHierarchy(filePath, position.line, position.character);
|
|
96
|
+
return Array.isArray(result) ? result : [];
|
|
97
|
+
}
|
|
98
|
+
async typeHierarchySupertypes(item) {
|
|
99
|
+
const result = await this.nativeClient.typeHierarchySupertypes(item);
|
|
100
|
+
return Array.isArray(result) ? result : [];
|
|
101
|
+
}
|
|
102
|
+
async typeHierarchySubtypes(item) {
|
|
103
|
+
const result = await this.nativeClient.typeHierarchySubtypes(item);
|
|
104
|
+
return Array.isArray(result) ? result : [];
|
|
105
|
+
}
|
|
106
|
+
async getDiagnostics(filePath, content) {
|
|
107
|
+
await this.openDocument(filePath, content);
|
|
108
|
+
return this.nativeClient.getDiagnostics(filePath);
|
|
109
|
+
}
|
|
79
110
|
hasCapability(_capability) {
|
|
80
111
|
return (this.initialized &&
|
|
81
112
|
(this.nativeClient.hasCapability?.(_capability) ?? true));
|
package/dist/lsp/config.d.ts
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
1
|
-
import type { LanguageServerConfig } from './types.js';
|
|
1
|
+
import type { LanguageServerConfig, LspServerSource } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Names of the servers octocode bundles (offline-ready, zero install). Single
|
|
4
|
+
* source for any "what's bundled" listing (e.g. the `lsp-server` CLI), derived
|
|
5
|
+
* from the maps above so it never drifts. pyright is the bundled Python server
|
|
6
|
+
* even though the native default command is `pylsp`.
|
|
7
|
+
*/
|
|
8
|
+
export declare const BUNDLED_SERVER_NAMES: readonly string[];
|
|
2
9
|
export declare function detectLanguageId(filePath: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Resolve the language-server config for a file, walking the resolution ladder
|
|
12
|
+
* behind a stable signature. The public contract is unchanged — callers still
|
|
13
|
+
* get a `LanguageServerConfig | null`; only the resolution behind it is richer.
|
|
14
|
+
*/
|
|
3
15
|
export declare function getLanguageServerForFile(filePath: string, workspaceRoot?: string): Promise<LanguageServerConfig | null>;
|
|
16
|
+
export interface ServerResolution {
|
|
17
|
+
config: LanguageServerConfig;
|
|
18
|
+
source: LspServerSource;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Full resolution including provenance, for status reporting. Order:
|
|
22
|
+
* L0/L1 explicit override + PATH (already applied natively in config.rs)
|
|
23
|
+
* L2 bundled JS server (npm dep, launched via current Node)
|
|
24
|
+
* L3 project-local / ecosystem (cargo/go/python/npm-global/mason/brew…)
|
|
25
|
+
* L4 managed download cache (~/.octocode/lsp, if pre-provisioned)
|
|
26
|
+
* Returns `source: 'unavailable'` (config still populated) when nothing on the
|
|
27
|
+
* machine provides the server, so the caller can report honest guidance.
|
|
28
|
+
*/
|
|
29
|
+
/** Check whether a bare command name is available on the current process PATH. */
|
|
30
|
+
export declare function isCommandOnPath(command: string): boolean;
|
|
31
|
+
export declare function resolveServerForFile(filePath: string, workspaceRoot?: string): Promise<ServerResolution | null>;
|
package/dist/lsp/config.js
CHANGED
|
@@ -1,7 +1,162 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import { nativeBinding } from './native.js';
|
|
4
|
+
import { discoverServer } from './serverDiscovery.js';
|
|
5
|
+
import { manifestServer, resolveCachedServer } from './serverManifest.js';
|
|
6
|
+
const requireFromPackage = createRequire(import.meta.url);
|
|
7
|
+
/**
|
|
8
|
+
* Pure-JS language servers shipped as npm dependencies of this package. When
|
|
9
|
+
* the named command is not otherwise available we launch the bundled CLI with
|
|
10
|
+
* the current Node runtime — so TS/JS, YAML, and the JSON/HTML/CSS data-format
|
|
11
|
+
* servers work in production with zero user install. Keyed by the bare command
|
|
12
|
+
* name the native spec table (`config.rs`) emits, so the native `args` carry
|
|
13
|
+
* over unchanged.
|
|
14
|
+
*/
|
|
15
|
+
const BUNDLED_JS_SERVERS = {
|
|
16
|
+
'typescript-language-server': 'typescript-language-server/lib/cli.mjs',
|
|
17
|
+
'yaml-language-server': 'yaml-language-server/bin/yaml-language-server',
|
|
18
|
+
'vscode-json-language-server': 'vscode-langservers-extracted/bin/vscode-json-language-server',
|
|
19
|
+
'vscode-html-language-server': 'vscode-langservers-extracted/bin/vscode-html-language-server',
|
|
20
|
+
'vscode-css-language-server': 'vscode-langservers-extracted/bin/vscode-css-language-server',
|
|
21
|
+
// PHP: native spec defaults to `intelephense` — command-keyed so native args carry over.
|
|
22
|
+
'intelephense': 'intelephense/lib/intelephense.js',
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Bundled servers selected by languageId when the native default command
|
|
26
|
+
* differs from the bundled one. Python's spec defaults to `pylsp`, but we ship
|
|
27
|
+
* `pyright` — a different launch contract (`pyright-langserver --stdio`) — so
|
|
28
|
+
* it is matched by language, not command name, and supplies its own args.
|
|
29
|
+
*/
|
|
30
|
+
const BUNDLED_BY_LANGUAGE = {
|
|
31
|
+
python: { cli: 'pyright/langserver.index.js', args: ['--stdio'] },
|
|
32
|
+
// Shell scripts: native spec table has no shellscript server spec, so this
|
|
33
|
+
// is the pre-native fallback path (detectLanguageId → 'shellscript' → here).
|
|
34
|
+
shellscript: { cli: 'bash-language-server/out/cli.js', args: ['start'] },
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Names of the servers octocode bundles (offline-ready, zero install). Single
|
|
38
|
+
* source for any "what's bundled" listing (e.g. the `lsp-server` CLI), derived
|
|
39
|
+
* from the maps above so it never drifts. pyright is the bundled Python server
|
|
40
|
+
* even though the native default command is `pylsp`.
|
|
41
|
+
*/
|
|
42
|
+
export const BUNDLED_SERVER_NAMES = [
|
|
43
|
+
...Object.keys(BUNDLED_JS_SERVERS),
|
|
44
|
+
'pyright',
|
|
45
|
+
'bash-language-server',
|
|
46
|
+
];
|
|
2
47
|
export function detectLanguageId(filePath) {
|
|
3
48
|
return nativeBinding.detectLanguageId(filePath) ?? 'plaintext';
|
|
4
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the language-server config for a file, walking the resolution ladder
|
|
52
|
+
* behind a stable signature. The public contract is unchanged — callers still
|
|
53
|
+
* get a `LanguageServerConfig | null`; only the resolution behind it is richer.
|
|
54
|
+
*/
|
|
5
55
|
export async function getLanguageServerForFile(filePath, workspaceRoot = process.cwd()) {
|
|
6
|
-
return (
|
|
56
|
+
return (await resolveServerForFile(filePath, workspaceRoot))?.config ?? null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Full resolution including provenance, for status reporting. Order:
|
|
60
|
+
* L0/L1 explicit override + PATH (already applied natively in config.rs)
|
|
61
|
+
* L2 bundled JS server (npm dep, launched via current Node)
|
|
62
|
+
* L3 project-local / ecosystem (cargo/go/python/npm-global/mason/brew…)
|
|
63
|
+
* L4 managed download cache (~/.octocode/lsp, if pre-provisioned)
|
|
64
|
+
* Returns `source: 'unavailable'` (config still populated) when nothing on the
|
|
65
|
+
* machine provides the server, so the caller can report honest guidance.
|
|
66
|
+
*/
|
|
67
|
+
/** Check whether a bare command name is available on the current process PATH. */
|
|
68
|
+
export function isCommandOnPath(command) {
|
|
69
|
+
return nativeBinding.isCommandAvailable(command);
|
|
70
|
+
}
|
|
71
|
+
export async function resolveServerForFile(filePath, workspaceRoot = process.cwd()) {
|
|
72
|
+
const base = nativeBinding.getLanguageServerForFile(filePath, workspaceRoot) ?? null;
|
|
73
|
+
// Pre-native: languages absent from the native spec table (e.g. shellscript)
|
|
74
|
+
// can still get a bundled server via BUNDLED_BY_LANGUAGE.
|
|
75
|
+
if (!base) {
|
|
76
|
+
const langId = nativeBinding.detectLanguageId(filePath) ?? '';
|
|
77
|
+
const preNative = langId ? BUNDLED_BY_LANGUAGE[langId] : undefined;
|
|
78
|
+
if (preNative) {
|
|
79
|
+
const cliPath = resolveBundledCli(preNative.cli);
|
|
80
|
+
if (cliPath) {
|
|
81
|
+
return {
|
|
82
|
+
config: {
|
|
83
|
+
command: process.execPath,
|
|
84
|
+
args: [cliPath, ...preNative.args],
|
|
85
|
+
languageId: langId,
|
|
86
|
+
},
|
|
87
|
+
source: 'bundled',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
// L0/L1: the native command already resolved (explicit override or on PATH).
|
|
94
|
+
if (nativeBinding.isCommandAvailable(base.command)) {
|
|
95
|
+
return { config: base, source: 'path' };
|
|
96
|
+
}
|
|
97
|
+
// L2: bundled JS server launched with the current Node runtime.
|
|
98
|
+
const bundled = withBundledJsServer(base);
|
|
99
|
+
if (bundled)
|
|
100
|
+
return { config: bundled, source: 'bundled' };
|
|
101
|
+
// L3: discover a server installed outside PATH (ecosystem/project-local dirs).
|
|
102
|
+
const discovered = discoverServer(base.command, workspaceRoot);
|
|
103
|
+
if (discovered) {
|
|
104
|
+
return {
|
|
105
|
+
config: { ...base, command: discovered.command },
|
|
106
|
+
source: discovered.source === 'project-local' ? 'project-local' : 'ecosystem',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// L4: a binary already sitting in the managed download cache.
|
|
110
|
+
const cached = resolveCachedServer(base.command);
|
|
111
|
+
if (cached) {
|
|
112
|
+
const manifest = manifestServer(base.command);
|
|
113
|
+
return {
|
|
114
|
+
config: {
|
|
115
|
+
...base,
|
|
116
|
+
command: cached,
|
|
117
|
+
args: manifest?.launchArgs ?? base.args,
|
|
118
|
+
},
|
|
119
|
+
source: 'managed-cache',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return { config: base, source: 'unavailable' };
|
|
123
|
+
}
|
|
124
|
+
function withBundledJsServer(config) {
|
|
125
|
+
// 1) Command-keyed: native command name matches the bundled package; keep
|
|
126
|
+
// the native args.
|
|
127
|
+
const byCommand = BUNDLED_JS_SERVERS[path.basename(config.command)];
|
|
128
|
+
if (byCommand) {
|
|
129
|
+
const cliPath = resolveBundledCli(byCommand);
|
|
130
|
+
if (cliPath) {
|
|
131
|
+
return {
|
|
132
|
+
...config,
|
|
133
|
+
command: process.execPath,
|
|
134
|
+
args: [cliPath, ...(config.args ?? [])],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// 2) Language-keyed: a different bundled server than the native default
|
|
139
|
+
// (e.g. pyright for Python); use the bundled server's own launch args.
|
|
140
|
+
const byLanguage = config.languageId
|
|
141
|
+
? BUNDLED_BY_LANGUAGE[config.languageId]
|
|
142
|
+
: undefined;
|
|
143
|
+
if (byLanguage) {
|
|
144
|
+
const cliPath = resolveBundledCli(byLanguage.cli);
|
|
145
|
+
if (cliPath) {
|
|
146
|
+
return {
|
|
147
|
+
...config,
|
|
148
|
+
command: process.execPath,
|
|
149
|
+
args: [cliPath, ...byLanguage.args],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
function resolveBundledCli(modulePath) {
|
|
156
|
+
try {
|
|
157
|
+
return requireFromPackage.resolve(modulePath);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
7
162
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects which host the octocode engine is running under, so LSP server
|
|
3
|
+
* resolution can prefer the editor's own servers when hosted in an IDE and
|
|
4
|
+
* fall back to standalone discovery in a plain terminal.
|
|
5
|
+
*
|
|
6
|
+
* Signals are environment variables the editor sets deliberately (see
|
|
7
|
+
* jonschlinkert/detect-terminal and VS Code's `terminalEnvironment.ts`).
|
|
8
|
+
* `TERM_PROGRAM` is the primary signal; the vendor `*_PID` / IPC-handle vars
|
|
9
|
+
* corroborate it. `TERM` is intentionally ignored — it is terminfo, not an
|
|
10
|
+
* editor identity.
|
|
11
|
+
*/
|
|
12
|
+
export type IdeHost = 'vscode' | 'cursor' | 'windsurf' | 'zed' | 'jetbrains' | 'terminal' | 'unknown';
|
|
13
|
+
export interface IdeContext {
|
|
14
|
+
host: IdeHost;
|
|
15
|
+
/** True when running inside an editor that ships language servers we could reuse. */
|
|
16
|
+
isIde: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function detectIdeContext(env?: NodeJS.ProcessEnv): IdeContext;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
function vscodeFork(env) {
|
|
2
|
+
// Cursor / Windsurf are VS Code forks and also report TERM_PROGRAM=vscode;
|
|
3
|
+
// disambiguate on their vendor markers before defaulting to upstream VS Code.
|
|
4
|
+
if (Object.keys(env).some(key => key.startsWith('CURSOR')))
|
|
5
|
+
return 'cursor';
|
|
6
|
+
if ('WINDSURF_PID' in env ||
|
|
7
|
+
Object.keys(env).some(key => key.startsWith('WINDSURF') || key.startsWith('CODEIUM'))) {
|
|
8
|
+
return 'windsurf';
|
|
9
|
+
}
|
|
10
|
+
return 'vscode';
|
|
11
|
+
}
|
|
12
|
+
export function detectIdeContext(env = process.env) {
|
|
13
|
+
const termProgram = (env.TERM_PROGRAM ?? '').toLowerCase();
|
|
14
|
+
if (termProgram === 'vscode' || 'VSCODE_PID' in env || 'VSCODE_GIT_IPC_HANDLE' in env) {
|
|
15
|
+
const host = vscodeFork(env);
|
|
16
|
+
return { host, isIde: true };
|
|
17
|
+
}
|
|
18
|
+
if (termProgram === 'zed' || 'ZED_TERM' in env) {
|
|
19
|
+
return { host: 'zed', isIde: true };
|
|
20
|
+
}
|
|
21
|
+
// JetBrains does not set TERM_PROGRAM; its tell is the JediTerm emulator.
|
|
22
|
+
if (env.TERMINAL_EMULATOR === 'JetBrains-JediTerm' || 'IDEA_INITIAL_DIRECTORY' in env) {
|
|
23
|
+
return { host: 'jetbrains', isIde: true };
|
|
24
|
+
}
|
|
25
|
+
if (termProgram === 'apple_terminal' || termProgram === 'iterm.app' || 'WT_SESSION' in env) {
|
|
26
|
+
return { host: 'terminal', isIde: false };
|
|
27
|
+
}
|
|
28
|
+
return { host: 'unknown', isIde: false };
|
|
29
|
+
}
|
package/dist/lsp/index.d.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
export { LSPClient } from './client.js';
|
|
2
|
-
export { detectLanguageId, getLanguageServerForFile } from './config.js';
|
|
3
|
-
export {
|
|
2
|
+
export { detectLanguageId, getLanguageServerForFile, isCommandOnPath, resolveServerForFile, BUNDLED_SERVER_NAMES, type ServerResolution, } from './config.js';
|
|
3
|
+
export { detectPlatformId, isMuslLinux, type PlatformId } from './platform.js';
|
|
4
|
+
export { detectIdeContext, type IdeContext, type IdeHost, } from './ideContext.js';
|
|
5
|
+
export { clearDiscoveryCache, discoverServer, discoverServerBatch, type DiscoveredServer, type DiscoverySource, } from './serverDiscovery.js';
|
|
6
|
+
export { cachedServerBinPath, isAutoDownloadable, listManifestServers, managedCacheRoot, manifestInstallHint, manifestServer, provisionMode, resolveCachedServer, type ArchiveKind, type ManifestAsset, type ManifestServer, type ProvisionMode, } from './serverManifest.js';
|
|
7
|
+
export { provisionServer, uninstallServer, type ProvisionResult, } from './serverProvisioner.js';
|
|
8
|
+
export { acquirePooledClient, getLspStatus, isLanguageServerAvailable, LSP_UNAVAILABLE_HINT, pooledClientCount, releaseAllPooledClients, releasePooledClientForFile, unavailableHintFor, TOOLCHAIN_SERVERS, type ToolchainServer, type LspStatusInput, type LspStatusResult, } from './manager.js';
|
|
4
9
|
export { resolveImportAliasDefinitions, SymbolResolver, type ImportAliasDefinitionInput, } from './resolver.js';
|
|
5
10
|
export { safeReadFile, validateLSPServerPath } from './validation.js';
|
|
6
11
|
export { resolveWorkspaceRootForFile } from './workspaceRoot.js';
|
|
7
|
-
export type { CallHierarchyItem, CodeSnippet, ExactPosition, FuzzyPosition, IncomingCall, InitializationOptions, LanguageServerCommand, LanguageServerConfig, LSPPaginationInfo, LSPRange, OutgoingCall, ReferenceLocation, ReferencesByFile, SymbolKind, UserLanguageServerConfig, } from './types.js';
|
|
12
|
+
export type { CallHierarchyItem, CodeSnippet, ExactPosition, FuzzyPosition, IncomingCall, InitializationOptions, LanguageServerCommand, LanguageServerConfig, LSPPaginationInfo, LSPRange, LspServerSource, OutgoingCall, ReferenceLocation, ReferencesByFile, SymbolKind, UserLanguageServerConfig, } from './types.js';
|
package/dist/lsp/index.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
export { LSPClient } from './client.js';
|
|
2
|
-
export { detectLanguageId, getLanguageServerForFile } from './config.js';
|
|
3
|
-
export {
|
|
2
|
+
export { detectLanguageId, getLanguageServerForFile, isCommandOnPath, resolveServerForFile, BUNDLED_SERVER_NAMES, } from './config.js';
|
|
3
|
+
export { detectPlatformId, isMuslLinux } from './platform.js';
|
|
4
|
+
export { detectIdeContext, } from './ideContext.js';
|
|
5
|
+
export { clearDiscoveryCache, discoverServer, discoverServerBatch, } from './serverDiscovery.js';
|
|
6
|
+
export { cachedServerBinPath, isAutoDownloadable, listManifestServers, managedCacheRoot, manifestInstallHint, manifestServer, provisionMode, resolveCachedServer, } from './serverManifest.js';
|
|
7
|
+
export { provisionServer, uninstallServer, } from './serverProvisioner.js';
|
|
8
|
+
export { acquirePooledClient, getLspStatus, isLanguageServerAvailable, LSP_UNAVAILABLE_HINT, pooledClientCount, releaseAllPooledClients, releasePooledClientForFile, unavailableHintFor, TOOLCHAIN_SERVERS, } from './manager.js';
|
|
4
9
|
export { resolveImportAliasDefinitions, SymbolResolver, } from './resolver.js';
|
|
5
10
|
export { safeReadFile, validateLSPServerPath } from './validation.js';
|
|
6
11
|
export { resolveWorkspaceRootForFile } from './workspaceRoot.js';
|
package/dist/lsp/manager.d.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { LSPClient } from './client.js';
|
|
2
2
|
import { type PoolKey } from './lspClientPool.js';
|
|
3
|
+
import type { LspServerSource } from './types.js';
|
|
3
4
|
export declare function isLanguageServerAvailable(filePath: string, workspaceRoot?: string): Promise<boolean>;
|
|
4
5
|
export declare const LSP_UNAVAILABLE_HINT = "No language server is available for this file, so no semantic results were returned. Install a matching language server or set the relevant OCTOCODE_*_SERVER_PATH environment variable. For a text-based search meanwhile, use localSearchCode.";
|
|
6
|
+
export interface ToolchainServer {
|
|
7
|
+
server: string;
|
|
8
|
+
languageId: string;
|
|
9
|
+
hint: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const TOOLCHAIN_SERVERS: readonly ToolchainServer[];
|
|
12
|
+
/** Honest, actionable guidance for a file whose server did not resolve. */
|
|
13
|
+
export declare function unavailableHintFor(languageId?: string, command?: string): string;
|
|
5
14
|
export declare function acquirePooledClient(workspaceRoot: string, filePath: string): Promise<LSPClient | null>;
|
|
6
15
|
export declare function releaseAllPooledClients(): Promise<void>;
|
|
7
16
|
export declare function releasePooledClientForFile(workspaceRoot: string, filePath: string): Promise<boolean>;
|
|
@@ -17,6 +26,8 @@ export type LspStatusResult = {
|
|
|
17
26
|
workspaceRoot?: string;
|
|
18
27
|
languageId?: string;
|
|
19
28
|
serverAvailable?: boolean;
|
|
29
|
+
/** Which layer of the resolution ladder provided the server (or `unavailable`). */
|
|
30
|
+
serverSource?: LspServerSource;
|
|
20
31
|
hints: string[];
|
|
21
32
|
};
|
|
22
33
|
export declare function getLspStatus(input?: LspStatusInput): Promise<LspStatusResult>;
|
package/dist/lsp/manager.js
CHANGED
|
@@ -1,16 +1,71 @@
|
|
|
1
1
|
import { LSPClient } from './client.js';
|
|
2
|
-
import { getLanguageServerForFile } from './config.js';
|
|
2
|
+
import { getLanguageServerForFile, resolveServerForFile, } from './config.js';
|
|
3
3
|
import { LspClientPool } from './lspClientPool.js';
|
|
4
|
-
import {
|
|
4
|
+
import { manifestInstallHint } from './serverManifest.js';
|
|
5
5
|
import { resolveWorkspaceRootForFile } from './workspaceRoot.js';
|
|
6
6
|
export async function isLanguageServerAvailable(filePath, workspaceRoot) {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
return false;
|
|
10
|
-
return nativeBinding.isCommandAvailable(serverConfig.command);
|
|
7
|
+
const resolution = await resolveServerForFile(filePath, workspaceRoot ?? process.cwd());
|
|
8
|
+
return resolution != null && resolution.source !== 'unavailable';
|
|
11
9
|
}
|
|
12
10
|
export const LSP_UNAVAILABLE_HINT = 'No language server is available for this file, so no semantic results were returned. Install a matching language server or set the relevant OCTOCODE_*_SERVER_PATH environment variable. For a text-based search meanwhile, use localSearchCode.';
|
|
11
|
+
export const TOOLCHAIN_SERVERS = [
|
|
12
|
+
{
|
|
13
|
+
server: 'gopls',
|
|
14
|
+
languageId: 'go',
|
|
15
|
+
hint: 'Install Go, then `go install golang.org/x/tools/gopls@latest` (gopls needs the Go toolchain at runtime).',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
server: 'jdtls',
|
|
19
|
+
languageId: 'java',
|
|
20
|
+
hint: 'Install a JDK/JRE 21+ and Eclipse JDT LS (https://download.eclipse.org/jdtls/).',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
server: 'sourcekit-lsp',
|
|
24
|
+
languageId: 'swift',
|
|
25
|
+
hint: 'Install Xcode or Xcode Command Line Tools (`xcode-select --install`); sourcekit-lsp ships at /usr/bin/sourcekit-lsp on macOS.',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
server: 'csharp-ls',
|
|
29
|
+
languageId: 'csharp',
|
|
30
|
+
hint: 'Install .NET SDK, then `dotnet tool install -g csharp-ls` (adds csharp-ls to ~/.dotnet/tools).',
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
const TOOLCHAIN_INSTALL_HINTS = Object.fromEntries(TOOLCHAIN_SERVERS.map(t => [t.languageId, t.hint]));
|
|
34
|
+
/** Honest, actionable guidance for a file whose server did not resolve. */
|
|
35
|
+
export function unavailableHintFor(languageId, command) {
|
|
36
|
+
const toolchain = languageId ? TOOLCHAIN_INSTALL_HINTS[languageId] : undefined;
|
|
37
|
+
if (toolchain)
|
|
38
|
+
return toolchain;
|
|
39
|
+
const manifest = command ? manifestInstallHint(command) : null;
|
|
40
|
+
if (manifest)
|
|
41
|
+
return manifest;
|
|
42
|
+
return LSP_UNAVAILABLE_HINT;
|
|
43
|
+
}
|
|
13
44
|
const POOL_IDLE_TIMEOUT_MS = parseInt(process.env.OCTOCODE_LSP_POOL_IDLE_MS || '60000', 10);
|
|
45
|
+
// Languages whose servers emit $/progress notifications and need waitForReady.
|
|
46
|
+
// TypeScript, Python, C/C++, and data-format servers (JSON/YAML/HTML/CSS)
|
|
47
|
+
// answer queries immediately after the LSP handshake — skipping waitForReady
|
|
48
|
+
// avoids burning the 2-second SETTLE_MS window for them.
|
|
49
|
+
const PROGRESS_LANGUAGES = new Set([
|
|
50
|
+
'go',
|
|
51
|
+
'rust',
|
|
52
|
+
'java',
|
|
53
|
+
'csharp',
|
|
54
|
+
'swift',
|
|
55
|
+
]);
|
|
56
|
+
// Per-language upper bound for $/progress drain (ms).
|
|
57
|
+
// These are ceilings — waitForReady returns as soon as the server goes idle.
|
|
58
|
+
const SERVER_READY_TIMEOUT_MS = {
|
|
59
|
+
go: 15_000,
|
|
60
|
+
rust: 60_000,
|
|
61
|
+
java: 120_000,
|
|
62
|
+
csharp: 30_000,
|
|
63
|
+
swift: 30_000,
|
|
64
|
+
};
|
|
65
|
+
const DEFAULT_READY_TIMEOUT_MS = 30_000;
|
|
66
|
+
function readyTimeoutForLanguage(languageId) {
|
|
67
|
+
return SERVER_READY_TIMEOUT_MS[languageId] ?? DEFAULT_READY_TIMEOUT_MS;
|
|
68
|
+
}
|
|
14
69
|
const sharedPool = new LspClientPool({
|
|
15
70
|
idleTimeoutMs: POOL_IDLE_TIMEOUT_MS,
|
|
16
71
|
factory: async (key) => {
|
|
@@ -20,6 +75,13 @@ const sharedPool = new LspClientPool({
|
|
|
20
75
|
const client = new LSPClient(serverConfig);
|
|
21
76
|
try {
|
|
22
77
|
await client.start();
|
|
78
|
+
// Wait for servers that do workspace-wide indexing before answering
|
|
79
|
+
// semantic queries. Servers that don't emit $/progress (TypeScript,
|
|
80
|
+
// Python, clangd) answer immediately after the handshake — we skip
|
|
81
|
+
// waitForReady for them to avoid the 2-second SETTLE_MS penalty.
|
|
82
|
+
if (PROGRESS_LANGUAGES.has(key.languageId)) {
|
|
83
|
+
await client.waitForReady(readyTimeoutForLanguage(key.languageId));
|
|
84
|
+
}
|
|
23
85
|
return client;
|
|
24
86
|
}
|
|
25
87
|
catch {
|
|
@@ -59,20 +121,20 @@ export async function getLspStatus(input = {}) {
|
|
|
59
121
|
};
|
|
60
122
|
}
|
|
61
123
|
const workspaceRoot = input.workspaceRoot ?? (await resolveWorkspaceRootForFile(input.filePath));
|
|
62
|
-
const
|
|
63
|
-
const languageId =
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
: false;
|
|
124
|
+
const resolution = await resolveServerForFile(input.filePath, workspaceRoot);
|
|
125
|
+
const languageId = resolution?.config.languageId;
|
|
126
|
+
const serverSource = resolution?.source ?? 'unavailable';
|
|
127
|
+
const serverAvailable = serverSource !== 'unavailable';
|
|
67
128
|
return {
|
|
68
129
|
...base,
|
|
69
130
|
filePath: input.filePath,
|
|
70
131
|
workspaceRoot,
|
|
71
132
|
languageId,
|
|
72
133
|
serverAvailable,
|
|
134
|
+
serverSource,
|
|
73
135
|
hints: serverAvailable
|
|
74
|
-
? [
|
|
75
|
-
: [
|
|
136
|
+
? [`Language server resolved for this file (source: ${serverSource}).`]
|
|
137
|
+
: [unavailableHintFor(languageId, resolution?.config.command)],
|
|
76
138
|
};
|
|
77
139
|
}
|
|
78
140
|
export function pooledClientCount() {
|
package/dist/lsp/native.d.ts
CHANGED
|
@@ -15,6 +15,11 @@ export type NativeLspClientBinding = {
|
|
|
15
15
|
prepareCallHierarchy(filePath: string, line: number, character: number): Promise<unknown>;
|
|
16
16
|
incomingCalls(item: unknown): Promise<unknown>;
|
|
17
17
|
outgoingCalls(item: unknown): Promise<unknown>;
|
|
18
|
+
workspaceSymbol(query: string): Promise<unknown>;
|
|
19
|
+
prepareTypeHierarchy(filePath: string, line: number, character: number): Promise<unknown>;
|
|
20
|
+
typeHierarchySupertypes(item: unknown): Promise<unknown>;
|
|
21
|
+
typeHierarchySubtypes(item: unknown): Promise<unknown>;
|
|
22
|
+
getDiagnostics(filePath: string): Promise<unknown>;
|
|
18
23
|
};
|
|
19
24
|
type NativeBinding = {
|
|
20
25
|
NativeLspClient: new (config: unknown) => NativeLspClientBinding;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical `{os}-{arch}[-musl]` platform identifier, matching the npm
|
|
3
|
+
* native-addon convention (the same keys esbuild / swc / the engine's own
|
|
4
|
+
* optionalDependencies use): `darwin-arm64`, `darwin-x64`, `linux-x64`,
|
|
5
|
+
* `linux-arm64`, `linux-x64-musl`, `linux-arm64-musl`, `win32-x64`,
|
|
6
|
+
* `win32-arm64`. This is the key the server download manifest is keyed on.
|
|
7
|
+
*/
|
|
8
|
+
export type PlatformId = 'darwin-arm64' | 'darwin-x64' | 'linux-x64' | 'linux-arm64' | 'linux-x64-musl' | 'linux-arm64-musl' | 'win32-x64' | 'win32-arm64';
|
|
9
|
+
/** True when the current Linux runtime links musl libc (Alpine etc.). */
|
|
10
|
+
export declare function isMuslLinux(): boolean;
|
|
11
|
+
/** Resolve (and cache) the canonical platform id for this machine. */
|
|
12
|
+
export declare function detectPlatformId(): PlatformId;
|
|
13
|
+
/**
|
|
14
|
+
* Executable file-name candidates for a bare command on this OS. On Windows a
|
|
15
|
+
* server may exist only as `gopls.exe` / `pyright.cmd`, so we expand against
|
|
16
|
+
* `PATHEXT`; on POSIX the bare name is the only candidate.
|
|
17
|
+
*/
|
|
18
|
+
export declare function executableNames(command: string): string[];
|
|
19
|
+
/** First existing path among `dir/<name>` for every executable-name candidate. */
|
|
20
|
+
export declare function firstExecutableIn(dir: string, command: string, join: (dir: string, name: string) => string): string | null;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
let cachedPlatformId;
|
|
3
|
+
/** True when the current Linux runtime links musl libc (Alpine etc.). */
|
|
4
|
+
export function isMuslLinux() {
|
|
5
|
+
if (process.platform !== 'linux')
|
|
6
|
+
return false;
|
|
7
|
+
// 1) Fastest, in-process: glibc runtime version is absent on musl.
|
|
8
|
+
try {
|
|
9
|
+
const report = process.report?.getReport();
|
|
10
|
+
if (report?.header) {
|
|
11
|
+
return report.header.glibcVersionRuntime == null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// process.report unavailable — fall through to the loader probe.
|
|
16
|
+
}
|
|
17
|
+
// 2) Dynamic-loader probe: musl ships `/lib/ld-musl-*.so.1`.
|
|
18
|
+
try {
|
|
19
|
+
return readdirSync('/lib').some(name => name.startsWith('ld-musl-'));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Resolve (and cache) the canonical platform id for this machine. */
|
|
26
|
+
export function detectPlatformId() {
|
|
27
|
+
if (cachedPlatformId)
|
|
28
|
+
return cachedPlatformId;
|
|
29
|
+
const arch = process.arch === 'x64' ? 'x64' : 'arm64';
|
|
30
|
+
if (process.platform === 'darwin') {
|
|
31
|
+
cachedPlatformId = `darwin-${arch}`;
|
|
32
|
+
}
|
|
33
|
+
else if (process.platform === 'win32') {
|
|
34
|
+
cachedPlatformId = `win32-${arch}`;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const suffix = isMuslLinux() ? '-musl' : '';
|
|
38
|
+
cachedPlatformId = `linux-${arch}${suffix}`;
|
|
39
|
+
}
|
|
40
|
+
return cachedPlatformId;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Executable file-name candidates for a bare command on this OS. On Windows a
|
|
44
|
+
* server may exist only as `gopls.exe` / `pyright.cmd`, so we expand against
|
|
45
|
+
* `PATHEXT`; on POSIX the bare name is the only candidate.
|
|
46
|
+
*/
|
|
47
|
+
export function executableNames(command) {
|
|
48
|
+
if (process.platform !== 'win32')
|
|
49
|
+
return [command];
|
|
50
|
+
const pathext = (process.env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM')
|
|
51
|
+
.split(';')
|
|
52
|
+
.map(ext => ext.trim().toLowerCase())
|
|
53
|
+
.filter(Boolean);
|
|
54
|
+
return [command, ...pathext.map(ext => `${command}${ext}`)];
|
|
55
|
+
}
|
|
56
|
+
/** First existing path among `dir/<name>` for every executable-name candidate. */
|
|
57
|
+
export function firstExecutableIn(dir, command, join) {
|
|
58
|
+
for (const name of executableNames(command)) {
|
|
59
|
+
const candidate = join(dir, name);
|
|
60
|
+
if (existsSync(candidate))
|
|
61
|
+
return candidate;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|