@octocodeai/octocode-engine 16.6.0 → 16.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +190 -249
- package/dist/lsp/client.d.ts +18 -9
- package/dist/lsp/client.js +35 -25
- 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/lspClientPool.d.ts +2 -0
- package/dist/lsp/lspClientPool.js +9 -4
- package/dist/lsp/manager.d.ts +11 -0
- package/dist/lsp/manager.js +72 -25
- package/dist/lsp/native.d.ts +2 -1
- package/dist/lsp/platform.d.ts +20 -0
- package/dist/lsp/platform.js +64 -0
- package/dist/lsp/resolver.js +10 -4
- package/dist/lsp/serverDiscovery.d.ts +63 -0
- package/dist/lsp/serverDiscovery.js +226 -0
- package/dist/lsp/serverManifest.d.ts +71 -0
- package/dist/lsp/serverManifest.js +116 -0
- package/dist/lsp/serverManifestData.d.ts +2 -0
- package/dist/lsp/serverManifestData.js +96 -0
- package/dist/lsp/serverProvisioner.d.ts +19 -0
- package/dist/lsp/serverProvisioner.js +262 -0
- package/dist/lsp/types.d.ts +18 -0
- package/dist/security/commandValidator.js +74 -10
- package/dist/security/contentSanitizer.js +4 -3
- package/dist/security/discoveryFilter.js +10 -0
- package/dist/security/filePatterns.js +6 -0
- package/dist/security/ignoredPathFilter.js +10 -2
- 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 +38 -37
- package/dist/security/registry.js +27 -9
- package/dist/security/securityConstants.d.ts +1 -1
- package/dist/security/securityConstants.js +22 -1
- package/dist/security/withSecurityValidation.js +18 -3
- package/docs/LSP_SERVER_LIFECYCLE.md +258 -0
- package/index.d.ts +8 -2
- package/package.json +54 -10
package/dist/lsp/client.d.ts
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
|
-
import type { CallHierarchyItem, CodeSnippet, ExactPosition, IncomingCall, LanguageServerConfig, OutgoingCall } from './types.js';
|
|
1
|
+
import type { CallHierarchyItem, CodeSnippet, ExactPosition, IncomingCall, LanguageServerConfig, LspReadiness, OutgoingCall } from './types.js';
|
|
2
2
|
export declare class LSPClient {
|
|
3
3
|
private readonly nativeClient;
|
|
4
|
+
private readonly command;
|
|
4
5
|
private initialized;
|
|
6
|
+
private lastReadiness;
|
|
5
7
|
constructor(config: LanguageServerConfig);
|
|
6
8
|
start(): Promise<void>;
|
|
7
9
|
stop(): Promise<void>;
|
|
8
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Wait for the server to finish post-`initialized` indexing and record the
|
|
12
|
+
* readiness signal. Runtime tolerance: until the native addon is rebuilt with
|
|
13
|
+
* the readiness return, the old binding resolves to `undefined` — treat that
|
|
14
|
+
* as the conservative `settledFallback` (we cannot confirm indexing finished).
|
|
15
|
+
*/
|
|
16
|
+
waitForReady(timeoutMs?: number): Promise<LspReadiness>;
|
|
17
|
+
/**
|
|
18
|
+
* The readiness recorded by the most recent `waitForReady`, or `undefined`
|
|
19
|
+
* if it was never called (e.g. servers that answer immediately and skip the
|
|
20
|
+
* readiness wait). A zero-results semantic query on a client whose readiness
|
|
21
|
+
* is not `progressIdle` may be "not indexed yet" rather than a true absence.
|
|
22
|
+
*/
|
|
23
|
+
getReadiness(): LspReadiness | undefined;
|
|
9
24
|
gotoDefinition(filePath: string, position: ExactPosition, content?: string): Promise<CodeSnippet[]>;
|
|
10
|
-
getDefinition(filePath: string, position: ExactPosition, content?: string): Promise<CodeSnippet[]>;
|
|
11
25
|
findReferences(filePath: string, position: ExactPosition, includeDeclaration?: boolean, content?: string): Promise<CodeSnippet[]>;
|
|
12
|
-
getHover(filePath: string, position: ExactPosition, content?: string): Promise<unknown>;
|
|
13
26
|
hover(filePath: string, position: ExactPosition, content?: string): Promise<unknown>;
|
|
14
|
-
getTypeDefinition(filePath: string, position: ExactPosition, content?: string): Promise<CodeSnippet[]>;
|
|
15
27
|
typeDefinition(filePath: string, position: ExactPosition, content?: string): Promise<CodeSnippet[]>;
|
|
16
|
-
getImplementation(filePath: string, position: ExactPosition, content?: string): Promise<CodeSnippet[]>;
|
|
17
28
|
implementation(filePath: string, position: ExactPosition, content?: string): Promise<CodeSnippet[]>;
|
|
18
|
-
getDocumentSymbols(filePath: string, content?: string): Promise<unknown>;
|
|
19
29
|
documentSymbols(filePath: string, content?: string): Promise<unknown>;
|
|
20
30
|
prepareCallHierarchy(filePath: string, position: ExactPosition, content?: string): Promise<CallHierarchyItem[]>;
|
|
21
31
|
getIncomingCalls(item: CallHierarchyItem): Promise<IncomingCall[]>;
|
|
@@ -25,9 +35,8 @@ export declare class LSPClient {
|
|
|
25
35
|
typeHierarchySupertypes(item: unknown): Promise<unknown[]>;
|
|
26
36
|
typeHierarchySubtypes(item: unknown): Promise<unknown[]>;
|
|
27
37
|
getDiagnostics(filePath: string, content?: string): Promise<unknown>;
|
|
28
|
-
hasCapability(
|
|
38
|
+
hasCapability(capability: string): boolean;
|
|
29
39
|
getRecentStderr(): string[];
|
|
30
40
|
openDocument(filePath: string, content?: string): Promise<void>;
|
|
31
|
-
ensureDocumentSynced(filePath: string, content?: string): Promise<void>;
|
|
32
41
|
closeDocument(filePath: string): Promise<void>;
|
|
33
42
|
}
|
package/dist/lsp/client.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
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;
|
|
8
|
+
lastReadiness;
|
|
6
9
|
constructor(config) {
|
|
10
|
+
this.command = config.command;
|
|
7
11
|
this.nativeClient = new nativeBinding.NativeLspClient({
|
|
8
12
|
command: config.command,
|
|
9
13
|
args: config.args,
|
|
@@ -14,6 +18,13 @@ export class LSPClient {
|
|
|
14
18
|
});
|
|
15
19
|
}
|
|
16
20
|
async start() {
|
|
21
|
+
// Security gate: never spawn a command that isn't a real, executable,
|
|
22
|
+
// non-shell server binary — even one resolved from the managed download
|
|
23
|
+
// cache. This is the single chokepoint before the native process spawn.
|
|
24
|
+
const validation = validateLSPServerPath(this.command);
|
|
25
|
+
if (!validation.isValid) {
|
|
26
|
+
throw new Error(`Refusing to start language server: ${validation.error ?? `invalid server path '${this.command}'`}`);
|
|
27
|
+
}
|
|
17
28
|
await this.nativeClient.start();
|
|
18
29
|
this.initialized = true;
|
|
19
30
|
}
|
|
@@ -21,13 +32,27 @@ export class LSPClient {
|
|
|
21
32
|
await this.nativeClient.stop();
|
|
22
33
|
this.initialized = false;
|
|
23
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Wait for the server to finish post-`initialized` indexing and record the
|
|
37
|
+
* readiness signal. Runtime tolerance: until the native addon is rebuilt with
|
|
38
|
+
* the readiness return, the old binding resolves to `undefined` — treat that
|
|
39
|
+
* as the conservative `settledFallback` (we cannot confirm indexing finished).
|
|
40
|
+
*/
|
|
24
41
|
async waitForReady(timeoutMs = 45_000) {
|
|
25
|
-
await this.nativeClient.waitForReady(timeoutMs);
|
|
42
|
+
const readiness = await this.nativeClient.waitForReady(timeoutMs);
|
|
43
|
+
this.lastReadiness = readiness ?? 'settledFallback';
|
|
44
|
+
return this.lastReadiness;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The readiness recorded by the most recent `waitForReady`, or `undefined`
|
|
48
|
+
* if it was never called (e.g. servers that answer immediately and skip the
|
|
49
|
+
* readiness wait). A zero-results semantic query on a client whose readiness
|
|
50
|
+
* is not `progressIdle` may be "not indexed yet" rather than a true absence.
|
|
51
|
+
*/
|
|
52
|
+
getReadiness() {
|
|
53
|
+
return this.lastReadiness;
|
|
26
54
|
}
|
|
27
55
|
async gotoDefinition(filePath, position, content) {
|
|
28
|
-
return this.getDefinition(filePath, position, content);
|
|
29
|
-
}
|
|
30
|
-
async getDefinition(filePath, position, content) {
|
|
31
56
|
await this.openDocument(filePath, content);
|
|
32
57
|
return (await this.nativeClient.getDefinition(filePath, position.line, position.character));
|
|
33
58
|
}
|
|
@@ -35,34 +60,22 @@ export class LSPClient {
|
|
|
35
60
|
await this.openDocument(filePath, content);
|
|
36
61
|
return (await this.nativeClient.getReferences(filePath, position.line, position.character, includeDeclaration));
|
|
37
62
|
}
|
|
38
|
-
async
|
|
63
|
+
async hover(filePath, position, content) {
|
|
39
64
|
await this.openDocument(filePath, content);
|
|
40
65
|
return this.nativeClient.getHover(filePath, position.line, position.character);
|
|
41
66
|
}
|
|
42
|
-
async
|
|
43
|
-
return this.getHover(filePath, position, content);
|
|
44
|
-
}
|
|
45
|
-
async getTypeDefinition(filePath, position, content) {
|
|
67
|
+
async typeDefinition(filePath, position, content) {
|
|
46
68
|
await this.openDocument(filePath, content);
|
|
47
69
|
return (await this.nativeClient.getTypeDefinition(filePath, position.line, position.character));
|
|
48
70
|
}
|
|
49
|
-
async
|
|
50
|
-
return this.getTypeDefinition(filePath, position, content);
|
|
51
|
-
}
|
|
52
|
-
async getImplementation(filePath, position, content) {
|
|
71
|
+
async implementation(filePath, position, content) {
|
|
53
72
|
await this.openDocument(filePath, content);
|
|
54
73
|
return (await this.nativeClient.getImplementation(filePath, position.line, position.character));
|
|
55
74
|
}
|
|
56
|
-
async
|
|
57
|
-
return this.getImplementation(filePath, position, content);
|
|
58
|
-
}
|
|
59
|
-
async getDocumentSymbols(filePath, content) {
|
|
75
|
+
async documentSymbols(filePath, content) {
|
|
60
76
|
await this.openDocument(filePath, content);
|
|
61
77
|
return this.nativeClient.getDocumentSymbols(filePath);
|
|
62
78
|
}
|
|
63
|
-
async documentSymbols(filePath, content) {
|
|
64
|
-
return this.getDocumentSymbols(filePath, content);
|
|
65
|
-
}
|
|
66
79
|
async prepareCallHierarchy(filePath, position, content) {
|
|
67
80
|
await this.openDocument(filePath, content);
|
|
68
81
|
const result = await this.nativeClient.prepareCallHierarchy(filePath, position.line, position.character);
|
|
@@ -97,17 +110,14 @@ export class LSPClient {
|
|
|
97
110
|
await this.openDocument(filePath, content);
|
|
98
111
|
return this.nativeClient.getDiagnostics(filePath);
|
|
99
112
|
}
|
|
100
|
-
hasCapability(
|
|
113
|
+
hasCapability(capability) {
|
|
101
114
|
return (this.initialized &&
|
|
102
|
-
(this.nativeClient.hasCapability?.(
|
|
115
|
+
(this.nativeClient.hasCapability?.(capability) ?? true));
|
|
103
116
|
}
|
|
104
117
|
getRecentStderr() {
|
|
105
118
|
return this.nativeClient.getRecentStderr?.() ?? [];
|
|
106
119
|
}
|
|
107
120
|
async openDocument(filePath, content) {
|
|
108
|
-
await this.ensureDocumentSynced(filePath, content ?? (await fs.readFile(filePath, 'utf8')));
|
|
109
|
-
}
|
|
110
|
-
async ensureDocumentSynced(filePath, content) {
|
|
111
121
|
await this.nativeClient.openDocument(filePath, content ?? (await fs.readFile(filePath, 'utf8')));
|
|
112
122
|
}
|
|
113
123
|
async closeDocument(filePath) {
|
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 = path.basename(config.command) === config.command && 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';
|
|
@@ -21,7 +21,9 @@ export declare class LspClientPool<T extends PooledClient> {
|
|
|
21
21
|
clearAll(): Promise<void>;
|
|
22
22
|
size(): number;
|
|
23
23
|
keys(): PoolKey[];
|
|
24
|
+
has(key: PoolKey): boolean;
|
|
24
25
|
private resetIdleTimer;
|
|
25
26
|
private startIdleTimer;
|
|
26
27
|
}
|
|
28
|
+
export declare function serializeKey(key: PoolKey): string;
|
|
27
29
|
export {};
|
|
@@ -53,6 +53,10 @@ export class LspClientPool {
|
|
|
53
53
|
keys() {
|
|
54
54
|
return [...this.entries.values()].map(entry => entry.key);
|
|
55
55
|
}
|
|
56
|
+
has(key) {
|
|
57
|
+
const k = serializeKey(key);
|
|
58
|
+
return this.entries.has(k) || this.inflight.has(k);
|
|
59
|
+
}
|
|
56
60
|
resetIdleTimer(k) {
|
|
57
61
|
const entry = this.entries.get(k);
|
|
58
62
|
if (!entry)
|
|
@@ -68,13 +72,14 @@ export class LspClientPool {
|
|
|
68
72
|
this.entries.delete(k);
|
|
69
73
|
void safeStop(entry.client);
|
|
70
74
|
}, this.options.idleTimeoutMs);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
// This package is Node-only (napi-rs). The timer is always a NodeJS.Timeout
|
|
76
|
+
// with an unref() method; calling it keeps the idle timer from preventing
|
|
77
|
+
// a clean process exit when no real work is in flight.
|
|
78
|
+
timer.unref?.();
|
|
74
79
|
return timer;
|
|
75
80
|
}
|
|
76
81
|
}
|
|
77
|
-
function serializeKey(key) {
|
|
82
|
+
export function serializeKey(key) {
|
|
78
83
|
return `${key.serverId ?? key.languageId}\u0000${key.workspaceRoot}`;
|
|
79
84
|
}
|
|
80
85
|
async function safeStop(client) {
|
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>;
|