pi-lsp-lite 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/index.ts +46 -25
- package/package.json +12 -4
- package/src/client.ts +8 -4
- package/src/config.ts +7 -3
- package/src/install-registry.ts +19 -6
- package/src/server-manager.ts +2 -1
- package/src/status.ts +95 -0
- package/src/util.ts +15 -16
package/README.md
CHANGED
|
@@ -50,10 +50,10 @@ The agent sees these too — they're appended to the tool result, so it can self
|
|
|
50
50
|
| `gopls` | Go | `go install golang.org/x/tools/gopls@latest` |
|
|
51
51
|
| `rust-analyzer` | Rust | `rustup component add rust-analyzer` |
|
|
52
52
|
| `typescript-language-server` | TypeScript/JS | `npm install -g typescript-language-server typescript` |
|
|
53
|
-
| `pylsp` | Python | `pip install python-lsp-server` |
|
|
53
|
+
| `pylsp` | Python | `python3 -m pip install python-lsp-server` / Windows: `py -m pip install python-lsp-server` |
|
|
54
54
|
| `clangd` | C/C++ | Xcode CLI tools / `apt install clangd` |
|
|
55
55
|
|
|
56
|
-
Missing a server? `/lsp-add` lets you configure any LSP server that speaks stdio. Or add it to
|
|
56
|
+
Missing a server? `/lsp-add` lets you configure any LSP server that speaks stdio. Or add it to global config (`~/.pi-lsp-lite.json`):
|
|
57
57
|
|
|
58
58
|
```json
|
|
59
59
|
{
|
|
@@ -70,7 +70,7 @@ Missing a server? `/lsp-add` lets you configure any LSP server that speaks stdio
|
|
|
70
70
|
|
|
71
71
|
## Configuration
|
|
72
72
|
|
|
73
|
-
Works without config.
|
|
73
|
+
Works without config. Use project config (`.pi-lsp-lite.json` or `.pi/lsp-lite.json`) for safe local tuning, and global config (`~/.pi-lsp-lite.json`) for trusted executable/server-shape changes like custom servers, `command`, `args`, `extensions`, and `rootPatterns`:
|
|
74
74
|
|
|
75
75
|
| Field | Description | Default |
|
|
76
76
|
|-------|-------------|---------|
|
|
@@ -80,7 +80,7 @@ Works without config. For customisation, create `.pi-lsp-lite.json` (project) or
|
|
|
80
80
|
| `diagnosticTimeout` | Global default timeout (ms) | `5000` |
|
|
81
81
|
| `documentIdleTimeout` | Close idle documents after (ms) | `120000` |
|
|
82
82
|
|
|
83
|
-
Project config merges over global.
|
|
83
|
+
Project config merges over global for safe tuning fields. Repositories can disable servers and tune timeouts/retries, but they cannot change the executable, argv, extensions, or root patterns for any existing server; put those trusted changes in global config.
|
|
84
84
|
|
|
85
85
|
## How it works
|
|
86
86
|
|
|
@@ -101,7 +101,7 @@ Servers are lazy (spawn on first edit), idle-shutdown after 240s, and clean up o
|
|
|
101
101
|
git clone https://github.com/mcphailtom/pi-lsp-lite
|
|
102
102
|
cd pi-lsp-lite && npm install
|
|
103
103
|
npm run check # typecheck
|
|
104
|
-
npm test # unit tests (
|
|
104
|
+
npm test # unit tests (no servers needed)
|
|
105
105
|
npm run test:integration # real server tests (needs servers on PATH)
|
|
106
106
|
```
|
|
107
107
|
|
package/index.ts
CHANGED
|
@@ -5,7 +5,8 @@ import { formatDiagnostics } from "./src/format.js";
|
|
|
5
5
|
import { DiagnosticSeverity } from "vscode-languageserver-protocol";
|
|
6
6
|
import { loadConfig, writeGlobalConfig, readGlobalConfig } from "./src/config.js";
|
|
7
7
|
import { fileUri, which, isInsideCwd } from "./src/util.js";
|
|
8
|
-
import { installRegistry } from "./src/install-registry.js";
|
|
8
|
+
import { installRegistry, installCommandFor } from "./src/install-registry.js";
|
|
9
|
+
import { buildServerStates, formatServerStates } from "./src/status.js";
|
|
9
10
|
import { resolve } from "node:path";
|
|
10
11
|
import { realpath } from "node:fs/promises";
|
|
11
12
|
import { fileURLToPath } from "node:url";
|
|
@@ -29,6 +30,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
async function currentServerStates() {
|
|
34
|
+
return buildServerStates({
|
|
35
|
+
builtins: builtinLanguages,
|
|
36
|
+
active: servers,
|
|
37
|
+
globalConfig: await readGlobalConfig(),
|
|
38
|
+
running: manager.status(),
|
|
39
|
+
installRegistry,
|
|
40
|
+
resolveCommand: which,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
pi.on("session_start", async (_event, ctx) => {
|
|
33
45
|
await initConfig(ctx.cwd);
|
|
34
46
|
});
|
|
@@ -71,19 +83,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
71
83
|
});
|
|
72
84
|
|
|
73
85
|
pi.registerCommand("lsp-status", {
|
|
74
|
-
description: "Show
|
|
86
|
+
description: "Show configured LSP servers, install state, and running processes",
|
|
75
87
|
handler: async (_args, ctx) => {
|
|
76
|
-
|
|
77
|
-
if (running.length === 0) {
|
|
78
|
-
ctx.ui.notify("pi-lsp-lite: no servers running", "info");
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
const lines = running.map((s) => {
|
|
82
|
-
const idle = Math.round((Date.now() - s.lastActivity) / 1000);
|
|
83
|
-
const up = Math.round(s.uptime / 1000);
|
|
84
|
-
return `${s.id} (pid ${s.pid}) root=${s.root} — ${s.openDocuments} open files, up ${up}s, idle ${idle}s`;
|
|
85
|
-
});
|
|
86
|
-
ctx.ui.notify(lines.join("\n"), "info");
|
|
88
|
+
ctx.ui.notify(formatServerStates(await currentServerStates()), "info");
|
|
87
89
|
},
|
|
88
90
|
});
|
|
89
91
|
|
|
@@ -169,18 +171,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
169
171
|
const rootPatterns = rootRaw ? rootRaw.split(",").map((r) => r.trim()).filter(Boolean) : [];
|
|
170
172
|
|
|
171
173
|
const resolved = await which(command);
|
|
172
|
-
if (!resolved) {
|
|
173
|
-
ctx.ui.notify(`pi-lsp-lite: "${command}" not found on PATH — server added but won't start until installed`, "warning");
|
|
174
|
-
}
|
|
175
|
-
|
|
176
174
|
await writeGlobalConfig({ servers: { [id]: { command, args, extensions, rootPatterns } } });
|
|
177
175
|
await initConfig(ctx.cwd);
|
|
178
|
-
|
|
176
|
+
|
|
177
|
+
if (!resolved) {
|
|
178
|
+
ctx.ui.notify(`pi-lsp-lite: configured server "${id}", but "${command}" is missing from PATH — install it manually before use`, "warning");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
ctx.ui.notify(`pi-lsp-lite: configured server "${id}" (${resolved})`, "info");
|
|
179
182
|
},
|
|
180
183
|
});
|
|
181
184
|
|
|
182
185
|
pi.registerCommand("lsp-remove", {
|
|
183
|
-
description: "
|
|
186
|
+
description: "Disable a language server",
|
|
184
187
|
handler: async (_args, ctx) => {
|
|
185
188
|
if (!ctx.hasUI) {
|
|
186
189
|
ctx.ui.notify("pi-lsp-lite: /lsp-remove requires interactive mode", "error");
|
|
@@ -193,10 +196,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
193
196
|
}
|
|
194
197
|
|
|
195
198
|
const ids = servers.map((s) => s.id);
|
|
196
|
-
const selected = await ctx.ui.select("
|
|
199
|
+
const selected = await ctx.ui.select("Disable which server?", ids);
|
|
197
200
|
if (!selected) return;
|
|
198
201
|
|
|
199
|
-
const confirmed = await ctx.ui.confirm("Confirm
|
|
202
|
+
const confirmed = await ctx.ui.confirm("Confirm disable", `Disable server "${selected}"?`);
|
|
200
203
|
if (!confirmed) return;
|
|
201
204
|
|
|
202
205
|
await writeGlobalConfig({ servers: { [selected]: { disabled: true } } });
|
|
@@ -246,7 +249,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
246
249
|
}
|
|
247
250
|
|
|
248
251
|
await initConfig(ctx.cwd);
|
|
249
|
-
|
|
252
|
+
|
|
253
|
+
if (isCurrentlyEnabled) {
|
|
254
|
+
ctx.ui.notify(`pi-lsp-lite: disabled server "${id}"`, "info");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const state = (await currentServerStates()).find((s) => s.id === id);
|
|
259
|
+
if (state?.installed === false) {
|
|
260
|
+
const installHint = state.installable ? "run /lsp-install" : "install it manually";
|
|
261
|
+
ctx.ui.notify(`pi-lsp-lite: enabled server "${id}", but "${state.command}" is missing from PATH — ${installHint} before use`, "warning");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (state?.installed === null) {
|
|
265
|
+
ctx.ui.notify(`pi-lsp-lite: enabled server "${id}", but its command is incomplete — check global config`, "warning");
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
ctx.ui.notify(`pi-lsp-lite: enabled server "${id}"`, "info");
|
|
250
269
|
},
|
|
251
270
|
});
|
|
252
271
|
|
|
@@ -263,13 +282,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
263
282
|
const lang = builtinLanguages.find((l) => l.id === id);
|
|
264
283
|
const binary = lang?.command ?? id;
|
|
265
284
|
const found = await which(binary);
|
|
266
|
-
return found ? null : { id, command: binary, installCmd: entry
|
|
285
|
+
return found ? null : { id, command: binary, installCmd: installCommandFor(entry), description: entry.description };
|
|
267
286
|
}),
|
|
268
287
|
);
|
|
269
288
|
const missing = checks.filter((c): c is NonNullable<typeof c> => c !== null);
|
|
270
289
|
|
|
271
290
|
if (missing.length === 0) {
|
|
272
|
-
ctx.ui.notify("pi-lsp-lite: all
|
|
291
|
+
ctx.ui.notify("pi-lsp-lite: all built-in installable servers are available; custom servers must be installed manually", "info");
|
|
273
292
|
return;
|
|
274
293
|
}
|
|
275
294
|
|
|
@@ -283,7 +302,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
283
302
|
const confirmed = await ctx.ui.confirm("Confirm install", `Run: ${selected.installCmd}`);
|
|
284
303
|
if (!confirmed) return;
|
|
285
304
|
|
|
286
|
-
const result =
|
|
305
|
+
const result = process.platform === "win32"
|
|
306
|
+
? await pi.exec(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", selected.installCmd])
|
|
307
|
+
: await pi.exec("sh", ["-c", selected.installCmd]);
|
|
287
308
|
if (result.code !== 0) {
|
|
288
309
|
ctx.ui.notify(`pi-lsp-lite: install failed (exit ${result.code})\n${result.stderr}`, "error");
|
|
289
310
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lsp-lite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "LSP diagnostics for pi — errors and warnings on every edit, same turn. Go, Rust, TypeScript, Python, C/C++.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -29,7 +29,9 @@
|
|
|
29
29
|
]
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"
|
|
32
|
+
"cross-spawn": "^7.0.6",
|
|
33
|
+
"vscode-languageserver-protocol": "~3.18.1",
|
|
34
|
+
"which": "^2.0.2"
|
|
33
35
|
},
|
|
34
36
|
"peerDependencies": {
|
|
35
37
|
"@earendil-works/pi-coding-agent": "*",
|
|
@@ -37,13 +39,19 @@
|
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|
|
39
41
|
"@earendil-works/pi-coding-agent": "^0.74.0",
|
|
40
|
-
"@types/
|
|
42
|
+
"@types/cross-spawn": "^6.0.6",
|
|
43
|
+
"@types/node": "^22.0.0",
|
|
44
|
+
"@types/which": "^2.0.2",
|
|
45
|
+
"cross-env": "^7.0.3",
|
|
41
46
|
"tsx": "^4.21.0",
|
|
42
47
|
"typescript": "^5.4.0"
|
|
43
48
|
},
|
|
44
49
|
"scripts": {
|
|
45
50
|
"check": "tsc --noEmit",
|
|
46
51
|
"test": "tsx --test test/*.test.ts",
|
|
47
|
-
"test:integration": "INTEGRATION=1 tsx --test test/*.test.ts test/integration/*.test.ts"
|
|
52
|
+
"test:integration": "cross-env INTEGRATION=1 tsx --test test/*.test.ts test/integration/*.test.ts"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=22.19.0"
|
|
48
56
|
}
|
|
49
57
|
}
|
package/src/client.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
DiagnosticSeverity,
|
|
14
14
|
type InitializeParams,
|
|
15
15
|
type Diagnostic,
|
|
16
|
-
} from "vscode-languageserver-protocol/node
|
|
16
|
+
} from "vscode-languageserver-protocol/node";
|
|
17
17
|
import type { ChildProcess } from "node:child_process";
|
|
18
18
|
import { fileUri } from "./util.js";
|
|
19
19
|
|
|
@@ -54,8 +54,12 @@ function countDiagnostics(diags: Diagnostic[]): { errors: number; warnings: numb
|
|
|
54
54
|
return { errors, warnings };
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
function diagnosticMessage(d: Diagnostic): string {
|
|
58
|
+
return typeof d.message === "string" ? d.message : d.message.value;
|
|
59
|
+
}
|
|
60
|
+
|
|
57
61
|
function diagnosticFingerprint(d: Diagnostic): string {
|
|
58
|
-
return `${d.severity}:${d.range.start.line}:${d.range.start.character}:${d
|
|
62
|
+
return `${d.severity}:${d.range.start.line}:${d.range.start.character}:${diagnosticMessage(d)}`;
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
function fingerprintSet(diags: Diagnostic[]): Set<string> {
|
|
@@ -202,7 +206,7 @@ export function createLspClient(child: ChildProcess): LspClient {
|
|
|
202
206
|
severity: first.severity ?? DiagnosticSeverity.Error,
|
|
203
207
|
line: first.range.start.line,
|
|
204
208
|
col: first.range.start.character,
|
|
205
|
-
message: first
|
|
209
|
+
message: diagnosticMessage(first),
|
|
206
210
|
...(first.source && { source: first.source }),
|
|
207
211
|
},
|
|
208
212
|
}),
|
|
@@ -285,7 +289,7 @@ export function createLspClient(child: ChildProcess): LspClient {
|
|
|
285
289
|
timer = setTimeout(() => reject(new Error("shutdown timed out")), SHUTDOWN_TIMEOUT_MS);
|
|
286
290
|
}),
|
|
287
291
|
]);
|
|
288
|
-
connection.sendNotification(ExitNotification.type);
|
|
292
|
+
await connection.sendNotification(ExitNotification.type);
|
|
289
293
|
} catch {
|
|
290
294
|
// timed out or server already exited
|
|
291
295
|
} finally {
|
package/src/config.ts
CHANGED
|
@@ -183,9 +183,13 @@ function mergeConfigs(
|
|
|
183
183
|
const existing = result.get(id);
|
|
184
184
|
if (existing) {
|
|
185
185
|
const { disabled: _, diagnosticTimeout: __, ...lspFields } = override;
|
|
186
|
-
if (source === "project"
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
if (source === "project") {
|
|
187
|
+
for (const field of ["command", "args", "extensions", "rootPatterns"] as const) {
|
|
188
|
+
if (lspFields[field] !== undefined) {
|
|
189
|
+
console.error(`[pi-lsp-lite] project config cannot override "${field}" for server "${id}" — ignoring (use global config instead)`);
|
|
190
|
+
delete lspFields[field];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
189
193
|
}
|
|
190
194
|
const defined = Object.fromEntries(
|
|
191
195
|
Object.entries(lspFields).filter(([, v]) => v !== undefined),
|
package/src/install-registry.ts
CHANGED
|
@@ -1,27 +1,40 @@
|
|
|
1
1
|
export interface InstallEntry {
|
|
2
|
-
command
|
|
2
|
+
// Per-platform install command; `win32` overrides `default` on Windows.
|
|
3
|
+
command: { default: string; win32?: string };
|
|
3
4
|
description: string;
|
|
4
5
|
}
|
|
5
6
|
|
|
6
7
|
export const installRegistry = new Map<string, InstallEntry>([
|
|
7
8
|
["go", {
|
|
8
|
-
command: "go install golang.org/x/tools/gopls@latest",
|
|
9
|
+
command: { default: "go install golang.org/x/tools/gopls@latest" },
|
|
9
10
|
description: "Go language server",
|
|
10
11
|
}],
|
|
11
12
|
["rust", {
|
|
12
|
-
command: "rustup component add rust-analyzer",
|
|
13
|
+
command: { default: "rustup component add rust-analyzer" },
|
|
13
14
|
description: "Rust language server",
|
|
14
15
|
}],
|
|
15
16
|
["typescript", {
|
|
16
|
-
command: "npm install -g typescript-language-server typescript",
|
|
17
|
+
command: { default: "npm install -g typescript-language-server typescript" },
|
|
17
18
|
description: "TypeScript/JavaScript language server",
|
|
18
19
|
}],
|
|
19
20
|
["python", {
|
|
20
|
-
command:
|
|
21
|
+
command: {
|
|
22
|
+
default: "python3 -m pip install python-lsp-server",
|
|
23
|
+
win32: "py -m pip install python-lsp-server",
|
|
24
|
+
},
|
|
21
25
|
description: "Python language server",
|
|
22
26
|
}],
|
|
23
27
|
["cpp", {
|
|
24
|
-
command:
|
|
28
|
+
command: {
|
|
29
|
+
default: "sudo apt-get install -y clangd || brew install llvm",
|
|
30
|
+
win32: "winget install -e --id LLVM.clangd",
|
|
31
|
+
},
|
|
25
32
|
description: "C/C++ language server",
|
|
26
33
|
}],
|
|
27
34
|
]);
|
|
35
|
+
|
|
36
|
+
// Resolve the install command for the current platform.
|
|
37
|
+
export function installCommandFor(entry: InstallEntry): string {
|
|
38
|
+
if (process.platform === "win32" && entry.command.win32) return entry.command.win32;
|
|
39
|
+
return entry.command.default;
|
|
40
|
+
}
|
package/src/server-manager.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type ChildProcess } from "node:child_process";
|
|
2
|
+
import spawn from "cross-spawn";
|
|
2
3
|
import { which, fileUri, findWorkspaceRoot } from "./util.js";
|
|
3
4
|
import { createLspClient, type LspClient, type DiagnosticResult } from "./client.js";
|
|
4
5
|
import { type LanguageServerConfig, languageIdForFile } from "./languages.js";
|
package/src/status.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { UserConfig } from "./config.js";
|
|
2
|
+
import type { InstallEntry } from "./install-registry.js";
|
|
3
|
+
import type { LanguageServerConfig } from "./languages.js";
|
|
4
|
+
import type { ServerStatus } from "./server-manager.js";
|
|
5
|
+
|
|
6
|
+
export interface ServerState {
|
|
7
|
+
id: string;
|
|
8
|
+
command: string | null;
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
installed: boolean | null;
|
|
11
|
+
installable: boolean;
|
|
12
|
+
running: ServerStatus[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface BuildServerStatesOptions {
|
|
16
|
+
builtins: LanguageServerConfig[];
|
|
17
|
+
active: LanguageServerConfig[];
|
|
18
|
+
globalConfig: UserConfig | null;
|
|
19
|
+
running: ServerStatus[];
|
|
20
|
+
installRegistry: Map<string, InstallEntry>;
|
|
21
|
+
resolveCommand(command: string): Promise<string | null>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const RESERVED_SERVER_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
25
|
+
|
|
26
|
+
function globalServerIds(globalConfig: UserConfig | null): string[] {
|
|
27
|
+
const servers = globalConfig?.servers;
|
|
28
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers)) return [];
|
|
29
|
+
return Object.keys(servers).filter((id) => !RESERVED_SERVER_KEYS.has(id));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function commandFor(id: string, active: Map<string, LanguageServerConfig>, builtins: Map<string, LanguageServerConfig>, globalConfig: UserConfig | null): string | null {
|
|
33
|
+
const activeConfig = active.get(id);
|
|
34
|
+
if (activeConfig) return activeConfig.command;
|
|
35
|
+
|
|
36
|
+
const globalCommand = globalConfig?.servers?.[id]?.command;
|
|
37
|
+
if (typeof globalCommand === "string" && globalCommand.length > 0) return globalCommand;
|
|
38
|
+
|
|
39
|
+
const builtin = builtins.get(id);
|
|
40
|
+
return builtin?.command ?? null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function buildServerStates(options: BuildServerStatesOptions): Promise<ServerState[]> {
|
|
44
|
+
const active = new Map(options.active.map((server) => [server.id, server]));
|
|
45
|
+
const builtins = new Map(options.builtins.map((server) => [server.id, server]));
|
|
46
|
+
const runningById = new Map<string, ServerStatus[]>();
|
|
47
|
+
|
|
48
|
+
for (const status of options.running) {
|
|
49
|
+
const entries = runningById.get(status.id) ?? [];
|
|
50
|
+
entries.push(status);
|
|
51
|
+
runningById.set(status.id, entries);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ids = new Set<string>([
|
|
55
|
+
...builtins.keys(),
|
|
56
|
+
...active.keys(),
|
|
57
|
+
...globalServerIds(options.globalConfig),
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const states = await Promise.all([...ids].sort().map(async (id): Promise<ServerState> => {
|
|
61
|
+
const command = commandFor(id, active, builtins, options.globalConfig);
|
|
62
|
+
const installed = command ? (await options.resolveCommand(command)) !== null : null;
|
|
63
|
+
return {
|
|
64
|
+
id,
|
|
65
|
+
command,
|
|
66
|
+
enabled: active.has(id),
|
|
67
|
+
installed,
|
|
68
|
+
installable: options.installRegistry.has(id),
|
|
69
|
+
running: runningById.get(id) ?? [],
|
|
70
|
+
};
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
return states;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function formatServerStates(states: ServerState[]): string {
|
|
77
|
+
if (states.length === 0) return "pi-lsp-lite: no servers configured";
|
|
78
|
+
|
|
79
|
+
return states.map((state) => {
|
|
80
|
+
const enabled = state.enabled ? "enabled" : "disabled";
|
|
81
|
+
const installed = state.installed === null ? "unknown" : state.installed ? "installed" : "missing";
|
|
82
|
+
const installHint = state.installed === false
|
|
83
|
+
? state.installable ? "installable via /lsp-install" : "manual install required"
|
|
84
|
+
: "";
|
|
85
|
+
const running = state.running.length > 0
|
|
86
|
+
? state.running.map((s) => {
|
|
87
|
+
const idle = Math.round((Date.now() - s.lastActivity) / 1000);
|
|
88
|
+
const up = Math.round(s.uptime / 1000);
|
|
89
|
+
return `running pid=${s.pid} root=${s.root} open=${s.openDocuments} up=${up}s idle=${idle}s`;
|
|
90
|
+
}).join("; ")
|
|
91
|
+
: "not running";
|
|
92
|
+
const command = state.command ? `cmd=${state.command}` : "cmd=unknown";
|
|
93
|
+
return [state.id, enabled, installed, running, command, installHint].filter(Boolean).join(" — ");
|
|
94
|
+
}).join("\n");
|
|
95
|
+
}
|
package/src/util.ts
CHANGED
|
@@ -1,29 +1,28 @@
|
|
|
1
|
-
import { access
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
2
|
import { join, dirname, relative, isAbsolute } from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
+
import which_ from "which";
|
|
4
5
|
|
|
5
6
|
export function fileUri(absolutePath: string): string {
|
|
6
7
|
return pathToFileURL(absolutePath).href;
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
// Find a binary on PATH. Delegates to the `which` package — the same resolver
|
|
11
|
+
// cross-spawn uses to locate the command it launches — so preflight resolution
|
|
12
|
+
// and the eventual spawn agree on every platform.
|
|
13
|
+
function isNotFoundError(err: unknown): boolean {
|
|
14
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
15
|
+
}
|
|
16
|
+
|
|
9
17
|
export async function which(command: string): Promise<string | null> {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return null;
|
|
18
|
+
try {
|
|
19
|
+
return await which_(command);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
if (!isNotFoundError(err)) {
|
|
22
|
+
console.error(`[pi-lsp-lite] failed to resolve command "${command}":`, err);
|
|
16
23
|
}
|
|
24
|
+
return null;
|
|
17
25
|
}
|
|
18
|
-
const pathDirs = (process.env.PATH ?? "").split(":");
|
|
19
|
-
for (const dir of pathDirs) {
|
|
20
|
-
const candidate = join(dir, command);
|
|
21
|
-
try {
|
|
22
|
-
await access(candidate, constants.X_OK);
|
|
23
|
-
return candidate;
|
|
24
|
-
} catch {}
|
|
25
|
-
}
|
|
26
|
-
return null;
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
export async function findWorkspaceRoot(filePath: string, rootPatterns: string[], cwd: string): Promise<string> {
|