pi-lens 3.1.2 → 3.2.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/CHANGELOG.md +55 -0
- package/README.md +16 -12
- package/clients/ast-grep-client.js +8 -1
- package/clients/ast-grep-client.ts +9 -1
- package/clients/biome-client.js +51 -38
- package/clients/biome-client.ts +60 -58
- package/clients/dependency-checker.js +30 -1
- package/clients/dependency-checker.ts +35 -1
- package/clients/dispatch/__tests__/runner-registration.test.ts +286 -282
- package/clients/dispatch/bus-dispatcher.js +15 -14
- package/clients/dispatch/bus-dispatcher.ts +32 -25
- package/clients/dispatch/dispatcher.js +18 -25
- package/clients/dispatch/dispatcher.test.ts +2 -1
- package/clients/dispatch/dispatcher.ts +17 -28
- package/clients/dispatch/plan.js +77 -32
- package/clients/dispatch/plan.ts +78 -32
- package/clients/dispatch/runners/ast-grep-napi.js +36 -376
- package/clients/dispatch/runners/ast-grep-napi.ts +60 -433
- package/clients/dispatch/runners/index.js +8 -4
- package/clients/dispatch/runners/index.ts +8 -4
- package/clients/dispatch/runners/lsp.js +65 -0
- package/clients/dispatch/runners/lsp.ts +125 -0
- package/clients/dispatch/runners/oxlint.js +2 -2
- package/clients/dispatch/runners/oxlint.ts +2 -2
- package/clients/dispatch/runners/pyright.js +24 -8
- package/clients/dispatch/runners/pyright.ts +28 -14
- package/clients/dispatch/runners/rust-clippy.js +2 -2
- package/clients/dispatch/runners/rust-clippy.ts +2 -4
- package/clients/dispatch/runners/tree-sitter.js +14 -2
- package/clients/dispatch/runners/tree-sitter.ts +15 -2
- package/clients/dispatch/runners/ts-lsp.js +3 -3
- package/clients/dispatch/runners/ts-lsp.ts +8 -5
- package/clients/dispatch/runners/yaml-rule-parser.js +292 -0
- package/clients/dispatch/runners/yaml-rule-parser.ts +338 -0
- package/clients/dispatch/types.js +3 -0
- package/clients/dispatch/types.ts +3 -0
- package/clients/formatters.js +67 -14
- package/clients/formatters.ts +68 -15
- package/clients/installer/index.js +78 -10
- package/clients/installer/index.ts +519 -426
- package/clients/jscpd-client.js +28 -0
- package/clients/jscpd-client.ts +41 -3
- package/clients/knip-client.js +30 -1
- package/clients/knip-client.ts +34 -2
- package/clients/lsp/__tests__/client.test.ts +64 -41
- package/clients/lsp/__tests__/config.test.ts +25 -17
- package/clients/lsp/__tests__/launch.test.ts +108 -43
- package/clients/lsp/__tests__/service.test.ts +76 -48
- package/clients/lsp/client.js +87 -2
- package/clients/lsp/client.ts +150 -6
- package/clients/lsp/config.js +8 -11
- package/clients/lsp/config.ts +24 -21
- package/clients/lsp/index.js +69 -0
- package/clients/lsp/index.ts +82 -0
- package/clients/lsp/interactive-install.js +19 -8
- package/clients/lsp/interactive-install.ts +52 -27
- package/clients/lsp/launch.js +182 -32
- package/clients/lsp/launch.ts +241 -38
- package/clients/lsp/path-utils.js +3 -46
- package/clients/lsp/path-utils.ts +11 -51
- package/clients/lsp/server.js +93 -71
- package/clients/lsp/server.ts +173 -131
- package/clients/path-utils.js +142 -0
- package/clients/path-utils.ts +153 -0
- package/clients/ruff-client.js +33 -4
- package/clients/ruff-client.ts +44 -13
- package/clients/safe-spawn.js +3 -1
- package/clients/safe-spawn.ts +3 -1
- package/clients/services/effect-integration.js +11 -7
- package/clients/services/effect-integration.ts +34 -26
- package/clients/sg-runner.js +51 -9
- package/clients/sg-runner.ts +58 -15
- package/clients/tree-sitter-client.js +12 -0
- package/clients/tree-sitter-client.ts +12 -0
- package/clients/typescript-client.js +6 -2
- package/clients/typescript-client.ts +9 -2
- package/commands/booboo.js +2 -4
- package/commands/booboo.ts +2 -4
- package/index.ts +377 -93
- package/package.json +2 -1
- package/rules/tree-sitter-queries/tsx/no-nested-links.yml +45 -0
- package/rules/tree-sitter-queries/typescript/constructor-super.yml +55 -0
- package/rules/tree-sitter-queries/typescript/debugger.yml +1 -1
- package/rules/tree-sitter-queries/typescript/no-dupe-class-members.yml +47 -0
- package/tsconfig.json +1 -1
- package/clients/__tests__/file-time.test.js +0 -216
- package/clients/__tests__/format-service.test.js +0 -245
- package/clients/__tests__/formatters.test.js +0 -271
- package/clients/agent-behavior-client.test.js +0 -94
- package/clients/ast-grep-client.test.js +0 -129
- package/clients/ast-grep-client.test.ts +0 -155
- package/clients/biome-client.test.js +0 -144
- package/clients/cache-manager.test.js +0 -197
- package/clients/complexity-client.test.js +0 -234
- package/clients/dependency-checker.test.js +0 -60
- package/clients/dispatch/__tests__/autofix-integration.test.js +0 -245
- package/clients/dispatch/__tests__/runner-registration.test.js +0 -236
- package/clients/dispatch/dispatcher.edge.test.js +0 -82
- package/clients/dispatch/dispatcher.format.test.js +0 -46
- package/clients/dispatch/dispatcher.inline.test.js +0 -74
- package/clients/dispatch/dispatcher.test.js +0 -115
- package/clients/dispatch/runners/architect.test.js +0 -138
- package/clients/dispatch/runners/ast-grep-napi.test.js +0 -106
- package/clients/dispatch/runners/oxlint.test.js +0 -230
- package/clients/dispatch/runners/pyright.test.js +0 -98
- package/clients/dispatch/runners/python-slop.test.js +0 -203
- package/clients/dispatch/runners/scan_codebase.test.js +0 -89
- package/clients/dispatch/runners/shellcheck.test.js +0 -98
- package/clients/dispatch/runners/spellcheck.test.js +0 -158
- package/clients/dispatch/runners/ts-slop.test.js +0 -180
- package/clients/dispatch/runners/ts-slop.test.ts +0 -230
- package/clients/dogfood.test.js +0 -201
- package/clients/file-kinds.test.js +0 -169
- package/clients/go-client.test.js +0 -127
- package/clients/jscpd-client.test.js +0 -127
- package/clients/knip-client.test.js +0 -112
- package/clients/lsp/__tests__/client.test.js +0 -325
- package/clients/lsp/__tests__/config.test.js +0 -166
- package/clients/lsp/__tests__/error-recovery.test.js +0 -213
- package/clients/lsp/__tests__/integration.test.js +0 -127
- package/clients/lsp/__tests__/launch.test.js +0 -260
- package/clients/lsp/__tests__/server.test.js +0 -259
- package/clients/lsp/__tests__/service.test.js +0 -417
- package/clients/metrics-client.test.js +0 -141
- package/clients/ruff-client.test.js +0 -132
- package/clients/rust-client.test.js +0 -108
- package/clients/sanitize.test.js +0 -177
- package/clients/secrets-scanner.test.js +0 -100
- package/clients/services/__tests__/effect-integration.test.js +0 -86
- package/clients/test-runner-client.test.js +0 -192
- package/clients/todo-scanner.test.js +0 -301
- package/clients/type-coverage-client.test.js +0 -105
- package/clients/typescript-client.codefix.test.js +0 -157
- package/clients/typescript-client.test.js +0 -105
- package/commands/clients/ast-grep-client.js +0 -250
- package/commands/clients/ast-grep-parser.js +0 -86
- package/commands/clients/ast-grep-rule-manager.js +0 -91
- package/commands/clients/ast-grep-types.js +0 -9
- package/commands/clients/biome-client.js +0 -380
- package/commands/clients/complexity-client.js +0 -667
- package/commands/clients/file-kinds.js +0 -177
- package/commands/clients/file-utils.js +0 -40
- package/commands/clients/jscpd-client.js +0 -169
- package/commands/clients/knip-client.js +0 -211
- package/commands/clients/ruff-client.js +0 -297
- package/commands/clients/safe-spawn.js +0 -88
- package/commands/clients/scan-utils.js +0 -83
- package/commands/clients/sg-runner.js +0 -190
- package/commands/clients/types.js +0 -11
- package/commands/clients/typescript-client.js +0 -505
- package/commands/rate.test.js +0 -119
- package/rules/ast-grep-rules/rules/no-dangerously-set-inner-html.yml +0 -13
- package/rules/ast-grep-rules/rules/no-debugger.yml +0 -12
- package/rules/ast-grep-rules/rules/no-eval.yml +0 -13
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Interactive LSP Installer
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Provides lazy auto-install with user prompt for common languages.
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* Features:
|
|
7
7
|
* - 30-second timeout with auto-accept
|
|
8
8
|
* - --auto-install flag for non-interactive mode
|
|
@@ -10,17 +10,20 @@
|
|
|
10
10
|
* - Only prompts for "common" languages (Go, Rust, YAML, JSON, Bash)
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import
|
|
14
|
-
import * as
|
|
15
|
-
import
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
import * as fs from "node:fs/promises";
|
|
15
|
+
import * as path from "node:path";
|
|
16
16
|
|
|
17
17
|
// Languages that support interactive auto-install prompt
|
|
18
|
-
const COMMON_LANGUAGES: Record<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
const COMMON_LANGUAGES: Record<
|
|
19
|
+
string,
|
|
20
|
+
{
|
|
21
|
+
toolId: string;
|
|
22
|
+
toolName: string;
|
|
23
|
+
installCommand: string;
|
|
24
|
+
packageName: string;
|
|
25
|
+
}
|
|
26
|
+
> = {
|
|
24
27
|
go: {
|
|
25
28
|
toolId: "gopls",
|
|
26
29
|
toolName: "Go Language Server (gopls)",
|
|
@@ -40,7 +43,7 @@ const COMMON_LANGUAGES: Record<string, {
|
|
|
40
43
|
packageName: "yaml-language-server",
|
|
41
44
|
},
|
|
42
45
|
json: {
|
|
43
|
-
toolId: "vscode-json-
|
|
46
|
+
toolId: "vscode-json-language-server",
|
|
44
47
|
toolName: "JSON Language Server",
|
|
45
48
|
installCommand: "npm install -g vscode-langservers-extracted",
|
|
46
49
|
packageName: "vscode-langservers-extracted",
|
|
@@ -68,7 +71,9 @@ function getCachePath(cwd: string): string {
|
|
|
68
71
|
/**
|
|
69
72
|
* Read cached install choices
|
|
70
73
|
*/
|
|
71
|
-
async function readChoices(
|
|
74
|
+
async function readChoices(
|
|
75
|
+
cwd: string,
|
|
76
|
+
): Promise<Record<string, InstallChoice>> {
|
|
72
77
|
try {
|
|
73
78
|
const cachePath = getCachePath(cwd);
|
|
74
79
|
const content = await fs.readFile(cachePath, "utf-8");
|
|
@@ -84,11 +89,11 @@ async function readChoices(cwd: string): Promise<Record<string, InstallChoice>>
|
|
|
84
89
|
async function saveChoice(
|
|
85
90
|
cwd: string,
|
|
86
91
|
toolId: string,
|
|
87
|
-
choice: "yes" | "no" | "auto"
|
|
92
|
+
choice: "yes" | "no" | "auto",
|
|
88
93
|
): Promise<void> {
|
|
89
94
|
const choices = await readChoices(cwd);
|
|
90
95
|
choices[toolId] = { choice, timestamp: Date.now() };
|
|
91
|
-
|
|
96
|
+
|
|
92
97
|
try {
|
|
93
98
|
const cachePath = getCachePath(cwd);
|
|
94
99
|
await fs.mkdir(path.dirname(cachePath), { recursive: true });
|
|
@@ -111,7 +116,7 @@ function promptUser(timeoutMs: number): Promise<"yes" | "no"> {
|
|
|
111
116
|
const onData = (data: Buffer | string) => {
|
|
112
117
|
const char = data.toString().trim().toLowerCase();
|
|
113
118
|
cleanup();
|
|
114
|
-
|
|
119
|
+
|
|
115
120
|
if (char === "y" || char === "\n" || char === "\r") {
|
|
116
121
|
resolve("yes");
|
|
117
122
|
} else if (char === "n") {
|
|
@@ -148,16 +153,21 @@ function promptUser(timeoutMs: number): Promise<"yes" | "no"> {
|
|
|
148
153
|
*/
|
|
149
154
|
function isAutoInstallEnabled(): boolean {
|
|
150
155
|
// Check environment variable or process arguments
|
|
151
|
-
return
|
|
152
|
-
process.
|
|
156
|
+
return (
|
|
157
|
+
process.env.PI_LENS_AUTO_INSTALL === "1" ||
|
|
158
|
+
process.argv.includes("--auto-install")
|
|
159
|
+
);
|
|
153
160
|
}
|
|
154
161
|
|
|
155
162
|
/**
|
|
156
163
|
* Attempt to install a tool
|
|
157
164
|
*/
|
|
158
|
-
async function installTool(
|
|
165
|
+
async function installTool(
|
|
166
|
+
toolId: string,
|
|
167
|
+
packageName: string,
|
|
168
|
+
): Promise<boolean> {
|
|
159
169
|
console.error(`[pi-lens] Installing ${toolId}...`);
|
|
160
|
-
|
|
170
|
+
|
|
161
171
|
return new Promise((resolve) => {
|
|
162
172
|
const proc = spawn("npm", ["install", "-g", packageName], {
|
|
163
173
|
stdio: "inherit",
|
|
@@ -169,7 +179,9 @@ async function installTool(toolId: string, packageName: string): Promise<boolean
|
|
|
169
179
|
console.error(`[pi-lens] ✓ ${toolId} installed successfully`);
|
|
170
180
|
resolve(true);
|
|
171
181
|
} else {
|
|
172
|
-
console.error(
|
|
182
|
+
console.error(
|
|
183
|
+
`[pi-lens] ✗ ${toolId} installation failed (exit code ${code})`,
|
|
184
|
+
);
|
|
173
185
|
resolve(false);
|
|
174
186
|
}
|
|
175
187
|
});
|
|
@@ -183,14 +195,14 @@ async function installTool(toolId: string, packageName: string): Promise<boolean
|
|
|
183
195
|
|
|
184
196
|
/**
|
|
185
197
|
* Prompt user for installation with timeout, or auto-install if flag set
|
|
186
|
-
*
|
|
198
|
+
*
|
|
187
199
|
* @param language - Language identifier (go, rust, yaml, json, bash)
|
|
188
200
|
* @param cwd - Project root
|
|
189
201
|
* @returns true if tool is/should be installed, false to skip
|
|
190
202
|
*/
|
|
191
203
|
export async function promptForInstall(
|
|
192
204
|
language: string,
|
|
193
|
-
cwd: string
|
|
205
|
+
cwd: string,
|
|
194
206
|
): Promise<boolean> {
|
|
195
207
|
const config = COMMON_LANGUAGES[language];
|
|
196
208
|
if (!config) {
|
|
@@ -201,21 +213,34 @@ export async function promptForInstall(
|
|
|
201
213
|
// Check cache first
|
|
202
214
|
const choices = await readChoices(cwd);
|
|
203
215
|
const cached = choices[config.toolId];
|
|
204
|
-
|
|
216
|
+
|
|
205
217
|
if (cached) {
|
|
206
218
|
// Cache valid for 30 days
|
|
207
219
|
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
|
208
220
|
if (Date.now() - cached.timestamp < thirtyDays) {
|
|
209
221
|
if (cached.choice === "yes" || cached.choice === "auto") {
|
|
210
|
-
|
|
222
|
+
// Verify binary actually exists before trusting cache
|
|
223
|
+
try {
|
|
224
|
+
const { execSync } = await import("node:child_process");
|
|
225
|
+
execSync(`which ${config.toolId}`, { stdio: "ignore" });
|
|
226
|
+
return true; // Binary exists, cache is valid
|
|
227
|
+
} catch {
|
|
228
|
+
// Binary not found, invalidate cache and continue to install
|
|
229
|
+
console.error(
|
|
230
|
+
`[pi-lens] Cached ${config.toolId} not found, re-installing...`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
return false; // User previously declined
|
|
211
235
|
}
|
|
212
|
-
return false;
|
|
213
236
|
}
|
|
214
237
|
}
|
|
215
238
|
|
|
216
239
|
// Check auto-install flag
|
|
217
240
|
if (isAutoInstallEnabled()) {
|
|
218
|
-
console.error(
|
|
241
|
+
console.error(
|
|
242
|
+
`[pi-lens] Auto-install enabled, installing ${config.toolName}...`,
|
|
243
|
+
);
|
|
219
244
|
await saveChoice(cwd, config.toolId, "auto");
|
|
220
245
|
return await installTool(config.toolId, config.packageName);
|
|
221
246
|
}
|
package/clients/lsp/launch.js
CHANGED
|
@@ -6,18 +6,91 @@
|
|
|
6
6
|
* - Node.js scripts (npx/bun)
|
|
7
7
|
* - Package manager execution
|
|
8
8
|
*/
|
|
9
|
-
import { spawn as nodeSpawn, } from "node:child_process";
|
|
9
|
+
import { execSync, spawn as nodeSpawn, } from "node:child_process";
|
|
10
|
+
import fs from "node:fs";
|
|
10
11
|
import path from "node:path";
|
|
11
|
-
// Helper to detect if running on Windows
|
|
12
12
|
const isWindows = process.platform === "win32";
|
|
13
|
+
/**
|
|
14
|
+
* Find binary in npm global directory
|
|
15
|
+
* Works around PATH caching issue after npm install -g
|
|
16
|
+
*/
|
|
17
|
+
function _findBinaryInNpmGlobal(command) {
|
|
18
|
+
try {
|
|
19
|
+
// Get npm global prefix
|
|
20
|
+
const prefix = execSync("npm prefix -g", { encoding: "utf-8" }).trim();
|
|
21
|
+
// On Windows, binaries are directly in the prefix dir
|
|
22
|
+
// On Unix, they're in prefix/bin
|
|
23
|
+
const binDir = isWindows ? prefix : path.join(prefix, "bin");
|
|
24
|
+
// Check for Windows variants
|
|
25
|
+
const candidates = isWindows
|
|
26
|
+
? [
|
|
27
|
+
path.join(binDir, `${command}.cmd`),
|
|
28
|
+
path.join(binDir, `${command}.exe`),
|
|
29
|
+
path.join(binDir, command),
|
|
30
|
+
]
|
|
31
|
+
: [path.join(binDir, command)];
|
|
32
|
+
for (const candidate of candidates) {
|
|
33
|
+
if (fs.existsSync(candidate)) {
|
|
34
|
+
return candidate;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Try to spawn a process, throwing immediately if it fails
|
|
45
|
+
*/
|
|
46
|
+
function trySpawn(command, args, cwd, env, needsShell) {
|
|
47
|
+
let proc;
|
|
48
|
+
if (needsShell) {
|
|
49
|
+
// Use shell mode with quoted command
|
|
50
|
+
const shellCommand = `"${command}" ${args.map((a) => (a.includes(" ") ? `"${a}"` : a)).join(" ")}`;
|
|
51
|
+
proc = nodeSpawn(shellCommand, [], {
|
|
52
|
+
cwd,
|
|
53
|
+
env,
|
|
54
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
55
|
+
detached: false,
|
|
56
|
+
windowsHide: true,
|
|
57
|
+
shell: true,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
// Use normal spawn without shell
|
|
62
|
+
proc = nodeSpawn(command, args, {
|
|
63
|
+
cwd,
|
|
64
|
+
env,
|
|
65
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
66
|
+
detached: false,
|
|
67
|
+
windowsHide: isWindows,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (!proc.stdin || !proc.stdout || !proc.stderr) {
|
|
71
|
+
throw new Error(`Failed to spawn LSP server: ${command}`);
|
|
72
|
+
}
|
|
73
|
+
// Check if process exited immediately (spawn failure - synchronous check)
|
|
74
|
+
if (proc.exitCode !== null || proc.killed) {
|
|
75
|
+
throw new Error(`LSP server ${command} exited immediately (code: ${proc.exitCode}). ` +
|
|
76
|
+
`The binary may be missing or corrupted.`);
|
|
77
|
+
}
|
|
78
|
+
return proc;
|
|
79
|
+
}
|
|
13
80
|
/**
|
|
14
81
|
* Attach error handler to a spawned process to prevent ENOENT crashes
|
|
15
82
|
* This catches "command not found" errors and other spawn failures
|
|
83
|
+
* Returns a promise that rejects if an immediate error occurs
|
|
16
84
|
*/
|
|
17
|
-
function _attachErrorHandler(proc, context) {
|
|
85
|
+
function _attachErrorHandler(proc, context, rejectOnImmediateError) {
|
|
18
86
|
proc.on("error", (err) => {
|
|
19
87
|
// Log the error but don't crash - the caller should handle this gracefully
|
|
20
88
|
console.error(`[lsp] Spawn error for ${context}:`, err.message);
|
|
89
|
+
// If we have a reject function and this is an immediate spawn error, reject
|
|
90
|
+
if (rejectOnImmediateError &&
|
|
91
|
+
err.code === "ENOENT") {
|
|
92
|
+
rejectOnImmediateError(err);
|
|
93
|
+
}
|
|
21
94
|
});
|
|
22
95
|
// Also handle unexpected exit (process crash after successful spawn)
|
|
23
96
|
proc.on("exit", (code, signal) => {
|
|
@@ -38,13 +111,14 @@ function _attachErrorHandler(proc, context) {
|
|
|
38
111
|
* - Uses absolute paths (relative paths fail in shell mode)
|
|
39
112
|
* - Uses shell: true for .cmd files
|
|
40
113
|
* - Uses windowsHide to prevent console window popup
|
|
114
|
+
* - Detects immediate spawn failures (ENOENT) before returning
|
|
41
115
|
*
|
|
42
116
|
* @param command - Command to run (e.g., "typescript-language-server")
|
|
43
117
|
* @param args - Arguments (e.g., ["--stdio"])
|
|
44
118
|
* @param options - Spawn options including cwd, env
|
|
45
119
|
* @returns LSPProcess handle
|
|
46
120
|
*/
|
|
47
|
-
export function launchLSP(command, args = [], options = {}) {
|
|
121
|
+
export async function launchLSP(command, args = [], options = {}) {
|
|
48
122
|
const cwd = String(options.cwd ?? process.cwd());
|
|
49
123
|
const env = { ...process.env, ...options.env };
|
|
50
124
|
// Resolve command path
|
|
@@ -56,31 +130,53 @@ export function launchLSP(command, args = [], options = {}) {
|
|
|
56
130
|
: command.includes(path.sep) || command.includes("/")
|
|
57
131
|
? path.resolve(cwd, command)
|
|
58
132
|
: command; // Let system find it via PATH
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
133
|
+
// Compute needsShell based on command
|
|
134
|
+
// On Windows, shell: true is needed for .cmd/.bat files and extensionless binaries
|
|
135
|
+
// .exe files can be spawned directly, but .cmd/.bat require shell interpretation
|
|
136
|
+
const hasScriptExtension = /\.(cmd|bat)$/i.test(resolvedCommand);
|
|
137
|
+
let needsShell = isWindows &&
|
|
138
|
+
(resolvedCommand.includes(" ") ||
|
|
139
|
+
hasScriptExtension ||
|
|
140
|
+
!/\.(exe|cmd|bat)$/i.test(resolvedCommand));
|
|
141
|
+
// Try to spawn the process
|
|
142
|
+
// If command not found, try npm global as fallback (handles PATH caching after install)
|
|
143
|
+
let spawnCommand = resolvedCommand;
|
|
144
|
+
// First, try to find in npm global if it's a simple command name
|
|
145
|
+
if (!path.isAbsolute(command) &&
|
|
146
|
+
!command.includes(path.sep) &&
|
|
147
|
+
!command.includes("/")) {
|
|
148
|
+
const npmGlobalPath = _findBinaryInNpmGlobal(command);
|
|
149
|
+
if (npmGlobalPath) {
|
|
150
|
+
spawnCommand = npmGlobalPath;
|
|
151
|
+
// Recompute needsShell for npm global path
|
|
152
|
+
const globalHasExt = /\.(exe|cmd|bat)$/i.test(spawnCommand);
|
|
153
|
+
needsShell = isWindows && (spawnCommand.includes(" ") || !globalHasExt);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
62
156
|
let proc;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const shellCommand = `"${resolvedCommand}" ${args.map((a) => (a.includes(" ") ? `"${a}"` : a)).join(" ")}`;
|
|
66
|
-
proc = nodeSpawn(shellCommand, [], {
|
|
67
|
-
cwd,
|
|
68
|
-
env,
|
|
69
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
70
|
-
detached: false,
|
|
71
|
-
windowsHide: true,
|
|
72
|
-
shell: true,
|
|
73
|
-
});
|
|
157
|
+
try {
|
|
158
|
+
proc = trySpawn(spawnCommand, args, cwd, env, needsShell);
|
|
74
159
|
}
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
160
|
+
catch (err) {
|
|
161
|
+
// If spawn failed with simple command, try npm global
|
|
162
|
+
if (!path.isAbsolute(command) &&
|
|
163
|
+
!command.includes(path.sep) &&
|
|
164
|
+
!command.includes("/")) {
|
|
165
|
+
const npmGlobalPath = _findBinaryInNpmGlobal(command);
|
|
166
|
+
if (npmGlobalPath && npmGlobalPath !== spawnCommand) {
|
|
167
|
+
console.error(`[lsp] Trying npm global: ${npmGlobalPath}`);
|
|
168
|
+
// Recompute needsShell for npm global path
|
|
169
|
+
const globalHasExt = /\.(exe|cmd|bat)$/i.test(npmGlobalPath);
|
|
170
|
+
const needsShellGlobal = isWindows && (npmGlobalPath.includes(" ") || !globalHasExt);
|
|
171
|
+
proc = trySpawn(npmGlobalPath, args, cwd, env, needsShellGlobal);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
throw err;
|
|
179
|
+
}
|
|
84
180
|
}
|
|
85
181
|
if (!proc.stdin || !proc.stdout || !proc.stderr) {
|
|
86
182
|
throw new Error(`Failed to spawn LSP server: ${command}`);
|
|
@@ -90,7 +186,35 @@ export function launchLSP(command, args = [], options = {}) {
|
|
|
90
186
|
throw new Error(`LSP server ${command} exited immediately (code: ${proc.exitCode}). ` +
|
|
91
187
|
`The binary may be missing or corrupted.`);
|
|
92
188
|
}
|
|
93
|
-
//
|
|
189
|
+
// For Windows and certain spawn failures, the error is async (ENOENT)
|
|
190
|
+
// We need to wait a small tick to catch immediate spawn failures
|
|
191
|
+
await new Promise((resolve, reject) => {
|
|
192
|
+
let settled = false;
|
|
193
|
+
// Attach error handler that can reject for immediate errors
|
|
194
|
+
proc.on("error", (err) => {
|
|
195
|
+
if (!settled && err.code === "ENOENT") {
|
|
196
|
+
settled = true;
|
|
197
|
+
reject(new Error(`LSP server binary not found: ${command}. ` +
|
|
198
|
+
`Install it or check your PATH.`));
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
// Also listen for immediate exit
|
|
202
|
+
proc.on("exit", (code) => {
|
|
203
|
+
if (!settled && code !== null) {
|
|
204
|
+
settled = true;
|
|
205
|
+
reject(new Error(`LSP server ${command} exited immediately with code ${code}. ` +
|
|
206
|
+
`The binary may be missing or corrupted.`));
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
// Give it a small window to fail immediately (ENOENT on Windows is fast)
|
|
210
|
+
setTimeout(() => {
|
|
211
|
+
if (!settled) {
|
|
212
|
+
settled = true;
|
|
213
|
+
resolve();
|
|
214
|
+
}
|
|
215
|
+
}, 50);
|
|
216
|
+
});
|
|
217
|
+
// Re-attach the permanent error handler now that we've passed the danger zone
|
|
94
218
|
_attachErrorHandler(proc, command);
|
|
95
219
|
return {
|
|
96
220
|
process: proc,
|
|
@@ -103,7 +227,7 @@ export function launchLSP(command, args = [], options = {}) {
|
|
|
103
227
|
/**
|
|
104
228
|
* Spawn via package manager (npx/bun)
|
|
105
229
|
*/
|
|
106
|
-
export function launchViaPackageManager(packageName, args = [], options = {}) {
|
|
230
|
+
export async function launchViaPackageManager(packageName, args = [], options = {}) {
|
|
107
231
|
// Prefer bun if available, fall back to npx (use .cmd on Windows)
|
|
108
232
|
const isWin = process.platform === "win32";
|
|
109
233
|
if (process.env.BUN_INSTALL) {
|
|
@@ -123,7 +247,33 @@ export function launchViaPackageManager(packageName, args = [], options = {}) {
|
|
|
123
247
|
windowsHide: true,
|
|
124
248
|
shell: true,
|
|
125
249
|
});
|
|
126
|
-
|
|
250
|
+
if (!proc.stdin || !proc.stdout || !proc.stderr) {
|
|
251
|
+
throw new Error(`Failed to spawn package manager for: ${packageName}`);
|
|
252
|
+
}
|
|
253
|
+
// Check for immediate spawn failure on Windows
|
|
254
|
+
await new Promise((resolve, reject) => {
|
|
255
|
+
let settled = false;
|
|
256
|
+
proc.on("error", (err) => {
|
|
257
|
+
if (!settled && err.code === "ENOENT") {
|
|
258
|
+
settled = true;
|
|
259
|
+
reject(new Error(`Package manager not found for: ${packageName}. ` +
|
|
260
|
+
`Install Node.js or check your PATH.`));
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
proc.on("exit", (code) => {
|
|
264
|
+
if (!settled && code !== null) {
|
|
265
|
+
settled = true;
|
|
266
|
+
reject(new Error(`Package manager exited immediately for: ${packageName} (code: ${code})`));
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
setTimeout(() => {
|
|
270
|
+
if (!settled) {
|
|
271
|
+
settled = true;
|
|
272
|
+
resolve();
|
|
273
|
+
}
|
|
274
|
+
}, 50);
|
|
275
|
+
});
|
|
276
|
+
// Attach permanent error handler
|
|
127
277
|
_attachErrorHandler(proc, packageName);
|
|
128
278
|
return {
|
|
129
279
|
process: proc,
|
|
@@ -138,13 +288,13 @@ export function launchViaPackageManager(packageName, args = [], options = {}) {
|
|
|
138
288
|
/**
|
|
139
289
|
* Spawn via Node.js directly
|
|
140
290
|
*/
|
|
141
|
-
export function launchViaNode(scriptPath, args = [], options = {}) {
|
|
291
|
+
export async function launchViaNode(scriptPath, args = [], options = {}) {
|
|
142
292
|
return launchLSP(process.execPath, [scriptPath, ...args], options);
|
|
143
293
|
}
|
|
144
294
|
/**
|
|
145
295
|
* Spawn via Python module
|
|
146
296
|
*/
|
|
147
|
-
export function launchViaPython(moduleName, args = [], options = {}) {
|
|
297
|
+
export async function launchViaPython(moduleName, args = [], options = {}) {
|
|
148
298
|
// On Windows, prefer 'py' launcher, fall back to 'python'
|
|
149
299
|
const pythonCmd = process.platform === "win32" ? "py" : "python3";
|
|
150
300
|
return launchLSP(pythonCmd, ["-m", moduleName, ...args], options);
|