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.
Files changed (154) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +16 -12
  3. package/clients/ast-grep-client.js +8 -1
  4. package/clients/ast-grep-client.ts +9 -1
  5. package/clients/biome-client.js +51 -38
  6. package/clients/biome-client.ts +60 -58
  7. package/clients/dependency-checker.js +30 -1
  8. package/clients/dependency-checker.ts +35 -1
  9. package/clients/dispatch/__tests__/runner-registration.test.ts +286 -282
  10. package/clients/dispatch/bus-dispatcher.js +15 -14
  11. package/clients/dispatch/bus-dispatcher.ts +32 -25
  12. package/clients/dispatch/dispatcher.js +18 -25
  13. package/clients/dispatch/dispatcher.test.ts +2 -1
  14. package/clients/dispatch/dispatcher.ts +17 -28
  15. package/clients/dispatch/plan.js +77 -32
  16. package/clients/dispatch/plan.ts +78 -32
  17. package/clients/dispatch/runners/ast-grep-napi.js +36 -376
  18. package/clients/dispatch/runners/ast-grep-napi.ts +60 -433
  19. package/clients/dispatch/runners/index.js +8 -4
  20. package/clients/dispatch/runners/index.ts +8 -4
  21. package/clients/dispatch/runners/lsp.js +65 -0
  22. package/clients/dispatch/runners/lsp.ts +125 -0
  23. package/clients/dispatch/runners/oxlint.js +2 -2
  24. package/clients/dispatch/runners/oxlint.ts +2 -2
  25. package/clients/dispatch/runners/pyright.js +24 -8
  26. package/clients/dispatch/runners/pyright.ts +28 -14
  27. package/clients/dispatch/runners/rust-clippy.js +2 -2
  28. package/clients/dispatch/runners/rust-clippy.ts +2 -4
  29. package/clients/dispatch/runners/tree-sitter.js +14 -2
  30. package/clients/dispatch/runners/tree-sitter.ts +15 -2
  31. package/clients/dispatch/runners/ts-lsp.js +3 -3
  32. package/clients/dispatch/runners/ts-lsp.ts +8 -5
  33. package/clients/dispatch/runners/yaml-rule-parser.js +292 -0
  34. package/clients/dispatch/runners/yaml-rule-parser.ts +338 -0
  35. package/clients/dispatch/types.js +3 -0
  36. package/clients/dispatch/types.ts +3 -0
  37. package/clients/formatters.js +67 -14
  38. package/clients/formatters.ts +68 -15
  39. package/clients/installer/index.js +78 -10
  40. package/clients/installer/index.ts +519 -426
  41. package/clients/jscpd-client.js +28 -0
  42. package/clients/jscpd-client.ts +41 -3
  43. package/clients/knip-client.js +30 -1
  44. package/clients/knip-client.ts +34 -2
  45. package/clients/lsp/__tests__/client.test.ts +64 -41
  46. package/clients/lsp/__tests__/config.test.ts +25 -17
  47. package/clients/lsp/__tests__/launch.test.ts +108 -43
  48. package/clients/lsp/__tests__/service.test.ts +76 -48
  49. package/clients/lsp/client.js +87 -2
  50. package/clients/lsp/client.ts +150 -6
  51. package/clients/lsp/config.js +8 -11
  52. package/clients/lsp/config.ts +24 -21
  53. package/clients/lsp/index.js +69 -0
  54. package/clients/lsp/index.ts +82 -0
  55. package/clients/lsp/interactive-install.js +19 -8
  56. package/clients/lsp/interactive-install.ts +52 -27
  57. package/clients/lsp/launch.js +182 -32
  58. package/clients/lsp/launch.ts +241 -38
  59. package/clients/lsp/path-utils.js +3 -46
  60. package/clients/lsp/path-utils.ts +11 -51
  61. package/clients/lsp/server.js +93 -71
  62. package/clients/lsp/server.ts +173 -131
  63. package/clients/path-utils.js +142 -0
  64. package/clients/path-utils.ts +153 -0
  65. package/clients/ruff-client.js +33 -4
  66. package/clients/ruff-client.ts +44 -13
  67. package/clients/safe-spawn.js +3 -1
  68. package/clients/safe-spawn.ts +3 -1
  69. package/clients/services/effect-integration.js +11 -7
  70. package/clients/services/effect-integration.ts +34 -26
  71. package/clients/sg-runner.js +51 -9
  72. package/clients/sg-runner.ts +58 -15
  73. package/clients/tree-sitter-client.js +12 -0
  74. package/clients/tree-sitter-client.ts +12 -0
  75. package/clients/typescript-client.js +6 -2
  76. package/clients/typescript-client.ts +9 -2
  77. package/commands/booboo.js +2 -4
  78. package/commands/booboo.ts +2 -4
  79. package/index.ts +377 -93
  80. package/package.json +2 -1
  81. package/rules/tree-sitter-queries/tsx/no-nested-links.yml +45 -0
  82. package/rules/tree-sitter-queries/typescript/constructor-super.yml +55 -0
  83. package/rules/tree-sitter-queries/typescript/debugger.yml +1 -1
  84. package/rules/tree-sitter-queries/typescript/no-dupe-class-members.yml +47 -0
  85. package/tsconfig.json +1 -1
  86. package/clients/__tests__/file-time.test.js +0 -216
  87. package/clients/__tests__/format-service.test.js +0 -245
  88. package/clients/__tests__/formatters.test.js +0 -271
  89. package/clients/agent-behavior-client.test.js +0 -94
  90. package/clients/ast-grep-client.test.js +0 -129
  91. package/clients/ast-grep-client.test.ts +0 -155
  92. package/clients/biome-client.test.js +0 -144
  93. package/clients/cache-manager.test.js +0 -197
  94. package/clients/complexity-client.test.js +0 -234
  95. package/clients/dependency-checker.test.js +0 -60
  96. package/clients/dispatch/__tests__/autofix-integration.test.js +0 -245
  97. package/clients/dispatch/__tests__/runner-registration.test.js +0 -236
  98. package/clients/dispatch/dispatcher.edge.test.js +0 -82
  99. package/clients/dispatch/dispatcher.format.test.js +0 -46
  100. package/clients/dispatch/dispatcher.inline.test.js +0 -74
  101. package/clients/dispatch/dispatcher.test.js +0 -115
  102. package/clients/dispatch/runners/architect.test.js +0 -138
  103. package/clients/dispatch/runners/ast-grep-napi.test.js +0 -106
  104. package/clients/dispatch/runners/oxlint.test.js +0 -230
  105. package/clients/dispatch/runners/pyright.test.js +0 -98
  106. package/clients/dispatch/runners/python-slop.test.js +0 -203
  107. package/clients/dispatch/runners/scan_codebase.test.js +0 -89
  108. package/clients/dispatch/runners/shellcheck.test.js +0 -98
  109. package/clients/dispatch/runners/spellcheck.test.js +0 -158
  110. package/clients/dispatch/runners/ts-slop.test.js +0 -180
  111. package/clients/dispatch/runners/ts-slop.test.ts +0 -230
  112. package/clients/dogfood.test.js +0 -201
  113. package/clients/file-kinds.test.js +0 -169
  114. package/clients/go-client.test.js +0 -127
  115. package/clients/jscpd-client.test.js +0 -127
  116. package/clients/knip-client.test.js +0 -112
  117. package/clients/lsp/__tests__/client.test.js +0 -325
  118. package/clients/lsp/__tests__/config.test.js +0 -166
  119. package/clients/lsp/__tests__/error-recovery.test.js +0 -213
  120. package/clients/lsp/__tests__/integration.test.js +0 -127
  121. package/clients/lsp/__tests__/launch.test.js +0 -260
  122. package/clients/lsp/__tests__/server.test.js +0 -259
  123. package/clients/lsp/__tests__/service.test.js +0 -417
  124. package/clients/metrics-client.test.js +0 -141
  125. package/clients/ruff-client.test.js +0 -132
  126. package/clients/rust-client.test.js +0 -108
  127. package/clients/sanitize.test.js +0 -177
  128. package/clients/secrets-scanner.test.js +0 -100
  129. package/clients/services/__tests__/effect-integration.test.js +0 -86
  130. package/clients/test-runner-client.test.js +0 -192
  131. package/clients/todo-scanner.test.js +0 -301
  132. package/clients/type-coverage-client.test.js +0 -105
  133. package/clients/typescript-client.codefix.test.js +0 -157
  134. package/clients/typescript-client.test.js +0 -105
  135. package/commands/clients/ast-grep-client.js +0 -250
  136. package/commands/clients/ast-grep-parser.js +0 -86
  137. package/commands/clients/ast-grep-rule-manager.js +0 -91
  138. package/commands/clients/ast-grep-types.js +0 -9
  139. package/commands/clients/biome-client.js +0 -380
  140. package/commands/clients/complexity-client.js +0 -667
  141. package/commands/clients/file-kinds.js +0 -177
  142. package/commands/clients/file-utils.js +0 -40
  143. package/commands/clients/jscpd-client.js +0 -169
  144. package/commands/clients/knip-client.js +0 -211
  145. package/commands/clients/ruff-client.js +0 -297
  146. package/commands/clients/safe-spawn.js +0 -88
  147. package/commands/clients/scan-utils.js +0 -83
  148. package/commands/clients/sg-runner.js +0 -190
  149. package/commands/clients/types.js +0 -11
  150. package/commands/clients/typescript-client.js +0 -505
  151. package/commands/rate.test.js +0 -119
  152. package/rules/ast-grep-rules/rules/no-dangerously-set-inner-html.yml +0 -13
  153. package/rules/ast-grep-rules/rules/no-debugger.yml +0 -12
  154. package/rules/ast-grep-rules/rules/no-eval.yml +0 -13
@@ -1,426 +1,519 @@
1
- /**
2
- * Auto-Installation System for pi-lens
3
- *
4
- * Minimal auto-install: Only TypeScript and Python ecosystems.
5
- * Other tools require manual installation with clear instructions.
6
- *
7
- * Auto-install (4 tools):
8
- * - typescript-language-server (TypeScript LSP)
9
- * - pyright (Python LSP)
10
- * - ruff (Python linting)
11
- * - @biomejs/biome (JS/TS/JSON linting/formatting)
12
- *
13
- * Manual install required (25+ tools):
14
- * - yaml-language-server: npm install -g yaml-language-server
15
- * - vscode-json-languageserver: npm install -g vscode-langservers-extracted
16
- * - bash-language-server: npm install -g bash-language-server
17
- * - svelte-language-server: npm install -g svelte-language-server
18
- * - vscode-eslint-language-server: npm install -g vscode-langservers-extracted
19
- * - vscode-css-languageserver: npm install -g vscode-langservers-extracted
20
- * - @prisma/language-server: npm install -g @prisma/language-server
21
- * - @ast-grep/cli: npm install -g @ast-grep/cli
22
- * - dockerfile-language-server: npm install -g dockerfile-language-server-nodejs
23
- * - @vue/language-server: npm install -g @vue/language-server
24
- * - And all language-specific servers (gopls, rust-analyzer, etc.)
25
- *
26
- * Strategies:
27
- * - npm packages via npx/bun
28
- * - pip packages
29
- * - GitHub releases (for platform-specific binaries - not yet implemented)
30
- */
31
-
32
- import { spawn } from "node:child_process";
33
- import fs from "node:fs/promises";
34
- import path from "node:path";
35
-
36
- // Global installation directory for pi-lens tools
37
- const TOOLS_DIR = path.join(process.cwd(), ".pi-lens", "tools");
38
-
39
- // --- Tool Definitions ---
40
-
41
- interface ToolDefinition {
42
- id: string;
43
- name: string;
44
- checkCommand: string;
45
- checkArgs: string[];
46
- installStrategy: "npm" | "pip" | "github";
47
- packageName?: string;
48
- binaryName?: string;
49
- }
50
-
51
- const TOOLS: ToolDefinition[] = [
52
- // Core LSP servers
53
- {
54
- id: "typescript-language-server",
55
- name: "TypeScript Language Server",
56
- checkCommand: "typescript-language-server",
57
- checkArgs: ["--version"],
58
- installStrategy: "npm",
59
- packageName: "typescript-language-server",
60
- binaryName: "typescript-language-server",
61
- },
62
- {
63
- id: "pyright",
64
- name: "Pyright",
65
- checkCommand: "pyright",
66
- checkArgs: ["--version"],
67
- installStrategy: "npm",
68
- packageName: "pyright",
69
- binaryName: "pyright",
70
- },
71
- // Linting/formatting tools
72
- {
73
- id: "ruff",
74
- name: "Ruff",
75
- checkCommand: "ruff",
76
- checkArgs: ["--version"],
77
- installStrategy: "pip",
78
- packageName: "ruff",
79
- binaryName: "ruff",
80
- },
81
- {
82
- id: "biome",
83
- name: "Biome",
84
- checkCommand: "biome",
85
- checkArgs: ["--version"],
86
- installStrategy: "npm",
87
- packageName: "@biomejs/biome",
88
- binaryName: "biome",
89
- },
90
- ];
91
-
92
- // --- Check Functions ---
93
-
94
- /**
95
- * Check if a command is available in PATH
96
- */
97
- async function isCommandAvailable(
98
- command: string,
99
- args: string[] = ["--version"],
100
- ): Promise<boolean> {
101
- return new Promise((resolve) => {
102
- // On Windows, use shell: true to handle .cmd files
103
- const isWindows = process.platform === "win32";
104
- const proc = isWindows
105
- ? spawn(`${command} ${args.join(" ")}`, [], {
106
- stdio: "ignore",
107
- shell: true,
108
- })
109
- : spawn(command, args, { stdio: "ignore" });
110
- proc.on("exit", (code) => resolve(code === 0));
111
- proc.on("error", () => resolve(false));
112
- });
113
- }
114
-
115
- /**
116
- * Check if a tool is installed (globally or locally)
117
- */
118
- export async function isToolInstalled(toolId: string): Promise<boolean> {
119
- const tool = TOOLS.find((t) => t.id === toolId);
120
- if (!tool) return false;
121
-
122
- // Check global PATH
123
- if (await isCommandAvailable(tool.checkCommand, tool.checkArgs)) {
124
- return true;
125
- }
126
-
127
- // Check local tools directory
128
- const localPath = path.join(
129
- TOOLS_DIR,
130
- "node_modules",
131
- ".bin",
132
- tool.binaryName || tool.id,
133
- );
134
- try {
135
- await fs.access(localPath);
136
- return true;
137
- } catch {
138
- return false;
139
- }
140
- }
141
-
142
- /**
143
- * Get the path to a tool (global or local)
144
- */
145
- export async function getToolPath(toolId: string): Promise<string | undefined> {
146
- const tool = TOOLS.find((t) => t.id === toolId);
147
- if (!tool) return undefined;
148
-
149
- // Check if global
150
- if (await isCommandAvailable(tool.checkCommand, tool.checkArgs)) {
151
- return tool.checkCommand;
152
- }
153
-
154
- // Check local
155
- const localPath = path.join(
156
- TOOLS_DIR,
157
- "node_modules",
158
- ".bin",
159
- tool.binaryName || tool.id,
160
- );
161
- try {
162
- await fs.access(localPath);
163
- return localPath;
164
- } catch {
165
- return undefined;
166
- }
167
- }
168
-
169
- // --- Verification Functions
170
-
171
- /**
172
- * Verify a tool binary actually works by running --version
173
- * This catches broken symlinks, partial installs, and corrupted binaries
174
- */
175
- async function verifyToolBinary(binPath: string): Promise<boolean> {
176
- return new Promise((resolve) => {
177
- // Add .cmd extension on Windows for the actual binary
178
- const isWindows = process.platform === "win32";
179
- const execPath = isWindows && !binPath.endsWith(".cmd")
180
- ? `${binPath}.cmd`
181
- : binPath;
182
-
183
- const proc = spawn(execPath, ["--version"], {
184
- timeout: 10000, // 10 second timeout for verification
185
- stdio: ["ignore", "pipe", "pipe"],
186
- shell: isWindows, // Required for .cmd wrappers on Windows
187
- });
188
-
189
- let stdout = "";
190
- let stderr = "";
191
-
192
- proc.stdout?.on("data", (data) => (stdout += data));
193
- proc.stderr?.on("data", (data) => (stderr += data));
194
-
195
- proc.on("exit", (code) => {
196
- if (code === 0) {
197
- console.error(`[auto-install] Verified: ${binPath} (version: ${stdout.trim()})`);
198
- resolve(true);
199
- } else {
200
- console.error(`[auto-install] Verification failed for ${binPath}: exit code ${code}, stderr: ${stderr}`);
201
- resolve(false);
202
- }
203
- });
204
-
205
- proc.on("error", (err) => {
206
- console.error(`[auto-install] Verification failed for ${binPath}: ${err.message}`);
207
- resolve(false);
208
- });
209
- });
210
- }
211
-
212
- // --- Installation Functions
213
-
214
- /**
215
- * Install an npm package tool
216
- */
217
- async function installNpmTool(
218
- packageName: string,
219
- binaryName: string
220
- ): Promise<string | undefined> {
221
- try {
222
- // Ensure tools directory exists
223
- await fs.mkdir(TOOLS_DIR, { recursive: true });
224
-
225
- // Create a minimal package.json if it doesn't exist
226
- const packageJsonPath = path.join(TOOLS_DIR, "package.json");
227
- try {
228
- await fs.access(packageJsonPath);
229
- } catch {
230
- await fs.writeFile(
231
- packageJsonPath,
232
- JSON.stringify({ name: "pi-lens-tools", version: "1.0.0" }, null, 2)
233
- );
234
- }
235
-
236
- console.error(`[auto-install] Installing ${packageName}...`);
237
-
238
- // Install via npm or bun (use .cmd on Windows)
239
- const isWindows = process.platform === "win32";
240
- const pm = process.env.BUN_INSTALL
241
- ? isWindows ? "bun.exe" : "bun"
242
- : isWindows ? "npm.cmd" : "npm";
243
- const proc = spawn(pm, ["install", packageName], {
244
- cwd: TOOLS_DIR,
245
- stdio: ["ignore", "pipe", "pipe"],
246
- shell: isWindows, // Required for .cmd files on Windows
247
- });
248
-
249
- return new Promise((resolve, reject) => {
250
- let stderr = "";
251
- proc.stderr?.on("data", (data) => (stderr += data));
252
-
253
- proc.on("exit", async (code) => {
254
- if (code === 0) {
255
- const binPath = path.join(TOOLS_DIR, "node_modules", ".bin", binaryName);
256
-
257
- // Make executable on Unix
258
- if (process.platform !== "win32") {
259
- try {
260
- await fs.chmod(binPath, 0o755);
261
- } catch { /* ignore */ }
262
- }
263
-
264
- // NEW: Verify the binary actually works before returning
265
- console.error(`[auto-install] Verifying ${binaryName}...`);
266
- const isValid = await verifyToolBinary(binPath);
267
- if (!isValid) {
268
- console.error(`[auto-install] ${packageName} installed but verification failed. The binary may be corrupted.`);
269
- // Clean up the broken installation
270
- try {
271
- const packagePath = path.join(TOOLS_DIR, "node_modules", packageName);
272
- await fs.rm(packagePath, { recursive: true, force: true });
273
- await fs.rm(binPath, { force: true });
274
- if (isWindows) {
275
- await fs.rm(`${binPath}.cmd`, { force: true });
276
- await fs.rm(`${binPath}.ps1`, { force: true });
277
- }
278
- } catch { /* ignore cleanup errors */ }
279
- resolve(undefined);
280
- return;
281
- }
282
-
283
- resolve(binPath);
284
- } else {
285
- reject(new Error(`Failed to install ${packageName}: ${stderr}`));
286
- }
287
- });
288
-
289
- proc.on("error", (err) => reject(err));
290
- });
291
- } catch (err) {
292
- console.error(`[auto-install] Failed to install npm tool ${packageName}:`, err);
293
- return undefined;
294
- }
295
- }
296
- /**
297
- * Install a pip package tool
298
- */
299
- async function installPipTool(
300
- packageName: string,
301
- ): Promise<string | undefined> {
302
- try {
303
- const pipCmd = process.platform === "win32" ? "pip" : "pip3";
304
- const isWindows = process.platform === "win32";
305
- const proc = spawn(pipCmd, ["install", "--user", packageName], {
306
- stdio: ["ignore", "pipe", "pipe"],
307
- shell: isWindows, // Required for .cmd files on Windows
308
- });
309
-
310
- return new Promise((resolve, reject) => {
311
- let stderr = "";
312
- proc.stderr?.on("data", (data) => (stderr += data));
313
-
314
- proc.on("exit", (code) => {
315
- if (code === 0) {
316
- resolve(packageName); // pip installs to PATH
317
- } else {
318
- reject(new Error(`Failed to install ${packageName}: ${stderr}`));
319
- }
320
- });
321
-
322
- proc.on("error", (err) => reject(err));
323
- });
324
- } catch (err) {
325
- console.error(
326
- `[auto-install] Failed to install pip tool ${packageName}:`,
327
- err,
328
- );
329
- return undefined;
330
- }
331
- }
332
-
333
- /**
334
- * Install a tool by ID
335
- */
336
- export async function installTool(toolId: string): Promise<boolean> {
337
- const tool = TOOLS.find((t) => t.id === toolId);
338
- if (!tool) {
339
- console.error(`[auto-install] Unknown tool: ${toolId}`);
340
- return false;
341
- }
342
-
343
- console.error(`[auto-install] Installing ${tool.name}...`);
344
-
345
- try {
346
- switch (tool.installStrategy) {
347
- case "npm": {
348
- if (!tool.packageName || !tool.binaryName) return false;
349
- const npmPath = await installNpmTool(tool.packageName, tool.binaryName);
350
- return npmPath !== undefined;
351
- }
352
-
353
- case "pip": {
354
- if (!tool.packageName) return false;
355
- const pipPath = await installPipTool(tool.packageName);
356
- return pipPath !== undefined;
357
- }
358
-
359
- default:
360
- console.error(
361
- `[auto-install] Unsupported strategy: ${tool.installStrategy}`,
362
- );
363
- return false;
364
- }
365
- } catch (err) {
366
- console.error(`[auto-install] Failed to install ${tool.name}:`, err);
367
- return false;
368
- }
369
- }
370
-
371
- /**
372
- * Ensure a tool is installed (check first, install if missing)
373
- */
374
- export async function ensureTool(toolId: string): Promise<string | undefined> {
375
- // Check if already installed
376
- const existingPath = await getToolPath(toolId);
377
- if (existingPath) {
378
- return existingPath;
379
- }
380
-
381
- // Try to install
382
- const installed = await installTool(toolId);
383
- if (!installed) {
384
- return undefined;
385
- }
386
-
387
- // Return the path after installation
388
- return getToolPath(toolId);
389
- }
390
-
391
- // --- Integration Helpers ---
392
-
393
- /**
394
- * Get environment with tool paths added
395
- */
396
- export async function getToolEnvironment(): Promise<NodeJS.ProcessEnv> {
397
- const localBin = path.join(TOOLS_DIR, "node_modules", ".bin");
398
- const currentPath = process.env.PATH || "";
399
- const separator = process.platform === "win32" ? ";" : ":";
400
-
401
- return {
402
- ...process.env,
403
- PATH: `${localBin}${separator}${currentPath}`,
404
- };
405
- }
406
-
407
- // --- Status Check ---
408
-
409
- /**
410
- * Check status of all managed tools
411
- */
412
- export async function checkAllTools(): Promise<
413
- Array<{ id: string; name: string; installed: boolean; path?: string }>
414
- > {
415
- const results = [];
416
- for (const tool of TOOLS) {
417
- const path = await getToolPath(tool.id);
418
- results.push({
419
- id: tool.id,
420
- name: tool.name,
421
- installed: path !== undefined,
422
- path,
423
- });
424
- }
425
- return results;
426
- }
1
+ /**
2
+ * Auto-Installation System for pi-lens
3
+ *
4
+ * Minimal auto-install: Core tools that run frequently.
5
+ * Other tools require manual installation with clear instructions.
6
+ *
7
+ * Auto-install (8 tools):
8
+ * - typescript-language-server (TypeScript LSP)
9
+ * - pyright (Python LSP)
10
+ * - ruff (Python linting)
11
+ * - @biomejs/biome (JS/TS/JSON linting/formatting)
12
+ * - madge (circular dependency detection)
13
+ * - jscpd (duplicate code detection)
14
+ * - @ast-grep/cli (structural code search)
15
+ * - knip (dead code detection)
16
+ *
17
+ * Manual install required (25+ tools):
18
+ * - yaml-language-server: npm install -g yaml-language-server
19
+ * - vscode-json-languageserver: npm install -g vscode-langservers-extracted
20
+ * - bash-language-server: npm install -g bash-language-server
21
+ * - svelte-language-server: npm install -g svelte-language-server
22
+ * - vscode-eslint-language-server: npm install -g vscode-langservers-extracted
23
+ * - vscode-css-languageserver: npm install -g vscode-langservers-extracted
24
+ * - @prisma/language-server: npm install -g @prisma/language-server
25
+ * - dockerfile-language-server: npm install -g dockerfile-language-server-nodejs
26
+ * - @vue/language-server: npm install -g @vue/language-server
27
+ * - And all language-specific servers (gopls, rust-analyzer, etc.)
28
+ *
29
+ * Strategies:
30
+ * - npm packages via npx/bun
31
+ * - pip packages
32
+ * - GitHub releases (for platform-specific binaries - not yet implemented)
33
+ */
34
+
35
+ import { spawn } from "node:child_process";
36
+ import fs from "node:fs/promises";
37
+ import path from "node:path";
38
+
39
+ // Global installation directory for pi-lens tools
40
+ const TOOLS_DIR = path.join(process.cwd(), ".pi-lens", "tools");
41
+
42
+ // --- Tool Definitions ---
43
+
44
+ interface ToolDefinition {
45
+ id: string;
46
+ name: string;
47
+ checkCommand: string;
48
+ checkArgs: string[];
49
+ installStrategy: "npm" | "pip" | "github";
50
+ packageName?: string;
51
+ binaryName?: string;
52
+ // GitHub release download fields
53
+ githubRepo?: string; // e.g., "clangd/clangd"
54
+ githubAssetMatch?: (platform: string, arch: string) => string | undefined;
55
+ }
56
+
57
+ const TOOLS: ToolDefinition[] = [
58
+ // Core LSP servers
59
+ {
60
+ id: "typescript-language-server",
61
+ name: "TypeScript Language Server",
62
+ checkCommand: "typescript-language-server",
63
+ checkArgs: ["--version"],
64
+ installStrategy: "npm",
65
+ packageName: "typescript-language-server",
66
+ binaryName: "typescript-language-server",
67
+ },
68
+ {
69
+ id: "pyright",
70
+ name: "Pyright",
71
+ checkCommand: "pyright",
72
+ checkArgs: ["--version"],
73
+ installStrategy: "npm",
74
+ packageName: "pyright",
75
+ binaryName: "pyright",
76
+ },
77
+ // Linting/formatting tools
78
+ {
79
+ id: "ruff",
80
+ name: "Ruff",
81
+ checkCommand: "ruff",
82
+ checkArgs: ["--version"],
83
+ installStrategy: "pip",
84
+ packageName: "ruff",
85
+ binaryName: "ruff",
86
+ },
87
+ {
88
+ id: "biome",
89
+ name: "Biome",
90
+ checkCommand: "biome",
91
+ checkArgs: ["--version"],
92
+ installStrategy: "npm",
93
+ packageName: "@biomejs/biome",
94
+ binaryName: "biome",
95
+ },
96
+ // Analysis tools (run at session start / turn end)
97
+ {
98
+ id: "madge",
99
+ name: "Madge",
100
+ checkCommand: "madge",
101
+ checkArgs: ["--version"],
102
+ installStrategy: "npm",
103
+ packageName: "madge",
104
+ binaryName: "madge",
105
+ },
106
+ {
107
+ id: "jscpd",
108
+ name: "jscpd",
109
+ checkCommand: "jscpd",
110
+ checkArgs: ["--version"],
111
+ installStrategy: "npm",
112
+ packageName: "jscpd",
113
+ binaryName: "jscpd",
114
+ },
115
+ // Structural search and dead code detection
116
+ {
117
+ id: "ast-grep",
118
+ name: "ast-grep CLI",
119
+ checkCommand: "sg",
120
+ checkArgs: ["--version"],
121
+ installStrategy: "npm",
122
+ packageName: "@ast-grep/cli",
123
+ binaryName: "sg",
124
+ },
125
+ {
126
+ id: "knip",
127
+ name: "Knip",
128
+ checkCommand: "knip",
129
+ checkArgs: ["--version"],
130
+ installStrategy: "npm",
131
+ packageName: "knip",
132
+ binaryName: "knip",
133
+ },
134
+ // GitHub release LSP servers
135
+ {
136
+ id: "clangd",
137
+ name: "clangd",
138
+ checkCommand: "clangd",
139
+ checkArgs: ["--version"],
140
+ installStrategy: "github",
141
+ binaryName: process.platform === "win32" ? "clangd.exe" : "clangd",
142
+ githubRepo: "clangd/clangd",
143
+ },
144
+ {
145
+ id: "lua-language-server",
146
+ name: "Lua Language Server",
147
+ checkCommand: "lua-language-server",
148
+ checkArgs: ["--version"],
149
+ installStrategy: "github",
150
+ binaryName:
151
+ process.platform === "win32"
152
+ ? "bin/lua-language-server.exe"
153
+ : "bin/lua-language-server",
154
+ githubRepo: "LuaLS/lua-language-server",
155
+ },
156
+ ];
157
+
158
+ // --- Check Functions ---
159
+
160
+ /**
161
+ * Check if a command is available in PATH
162
+ */
163
+ async function isCommandAvailable(
164
+ command: string,
165
+ args: string[] = ["--version"],
166
+ ): Promise<boolean> {
167
+ return new Promise((resolve) => {
168
+ // On Windows, use shell: true to handle .cmd files
169
+ const isWindows = process.platform === "win32";
170
+ const proc = isWindows
171
+ ? spawn(`${command} ${args.join(" ")}`, [], {
172
+ stdio: "ignore",
173
+ shell: true,
174
+ })
175
+ : spawn(command, args, { stdio: "ignore" });
176
+ proc.on("exit", (code) => resolve(code === 0));
177
+ proc.on("error", () => resolve(false));
178
+ });
179
+ }
180
+
181
+ /**
182
+ * Check if a tool is installed (globally or locally)
183
+ */
184
+ export async function isToolInstalled(toolId: string): Promise<boolean> {
185
+ const tool = TOOLS.find((t) => t.id === toolId);
186
+ if (!tool) return false;
187
+
188
+ // Check global PATH
189
+ if (await isCommandAvailable(tool.checkCommand, tool.checkArgs)) {
190
+ return true;
191
+ }
192
+
193
+ // Check local tools directory
194
+ const localPath = path.join(
195
+ TOOLS_DIR,
196
+ "node_modules",
197
+ ".bin",
198
+ tool.binaryName || tool.id,
199
+ );
200
+ try {
201
+ await fs.access(localPath);
202
+ return true;
203
+ } catch {
204
+ return false;
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Get the path to a tool (global or local)
210
+ */
211
+ export async function getToolPath(toolId: string): Promise<string | undefined> {
212
+ const tool = TOOLS.find((t) => t.id === toolId);
213
+ if (!tool) return undefined;
214
+
215
+ // Check if global
216
+ if (await isCommandAvailable(tool.checkCommand, tool.checkArgs)) {
217
+ return tool.checkCommand;
218
+ }
219
+
220
+ // Check local
221
+ const localPath = path.join(
222
+ TOOLS_DIR,
223
+ "node_modules",
224
+ ".bin",
225
+ tool.binaryName || tool.id,
226
+ );
227
+ try {
228
+ await fs.access(localPath);
229
+ return localPath;
230
+ } catch {
231
+ return undefined;
232
+ }
233
+ }
234
+
235
+ // --- Verification Functions
236
+
237
+ /**
238
+ * Verify a tool binary actually works by running --version
239
+ * This catches broken symlinks, partial installs, and corrupted binaries
240
+ */
241
+ async function verifyToolBinary(binPath: string): Promise<boolean> {
242
+ return new Promise((resolve) => {
243
+ // Add .cmd extension on Windows for the actual binary
244
+ const isWindows = process.platform === "win32";
245
+ const execPath =
246
+ isWindows && !binPath.endsWith(".cmd") ? `${binPath}.cmd` : binPath;
247
+
248
+ const proc = spawn(execPath, ["--version"], {
249
+ timeout: 10000, // 10 second timeout for verification
250
+ stdio: ["ignore", "pipe", "pipe"],
251
+ shell: isWindows, // Required for .cmd wrappers on Windows
252
+ });
253
+
254
+ let stdout = "";
255
+ let stderr = "";
256
+
257
+ proc.stdout?.on("data", (data) => (stdout += data));
258
+ proc.stderr?.on("data", (data) => (stderr += data));
259
+
260
+ proc.on("exit", (code) => {
261
+ if (code === 0) {
262
+ console.error(
263
+ `[auto-install] Verified: ${binPath} (version: ${stdout.trim()})`,
264
+ );
265
+ resolve(true);
266
+ } else {
267
+ console.error(
268
+ `[auto-install] Verification failed for ${binPath}: exit code ${code}, stderr: ${stderr}`,
269
+ );
270
+ resolve(false);
271
+ }
272
+ });
273
+
274
+ proc.on("error", (err) => {
275
+ console.error(
276
+ `[auto-install] Verification failed for ${binPath}: ${err.message}`,
277
+ );
278
+ resolve(false);
279
+ });
280
+ });
281
+ }
282
+
283
+ // --- Installation Functions
284
+
285
+ /**
286
+ * Install an npm package tool
287
+ */
288
+ async function installNpmTool(
289
+ packageName: string,
290
+ binaryName: string,
291
+ ): Promise<string | undefined> {
292
+ try {
293
+ // Ensure tools directory exists
294
+ await fs.mkdir(TOOLS_DIR, { recursive: true });
295
+
296
+ // Create a minimal package.json if it doesn't exist
297
+ const packageJsonPath = path.join(TOOLS_DIR, "package.json");
298
+ try {
299
+ await fs.access(packageJsonPath);
300
+ } catch {
301
+ await fs.writeFile(
302
+ packageJsonPath,
303
+ JSON.stringify({ name: "pi-lens-tools", version: "1.0.0" }, null, 2),
304
+ );
305
+ }
306
+
307
+ console.error(`[auto-install] Installing ${packageName}...`);
308
+
309
+ // Install via npm or bun (use .cmd on Windows)
310
+ const isWindows = process.platform === "win32";
311
+ const pm = process.env.BUN_INSTALL
312
+ ? isWindows
313
+ ? "bun.exe"
314
+ : "bun"
315
+ : isWindows
316
+ ? "npm.cmd"
317
+ : "npm";
318
+ const proc = spawn(pm, ["install", packageName], {
319
+ cwd: TOOLS_DIR,
320
+ stdio: ["ignore", "pipe", "pipe"],
321
+ shell: isWindows, // Required for .cmd files on Windows
322
+ });
323
+
324
+ return new Promise((resolve, reject) => {
325
+ let stderr = "";
326
+ proc.stderr?.on("data", (data) => (stderr += data));
327
+
328
+ proc.on("exit", async (code) => {
329
+ if (code === 0) {
330
+ const binPath = path.join(
331
+ TOOLS_DIR,
332
+ "node_modules",
333
+ ".bin",
334
+ binaryName,
335
+ );
336
+
337
+ // Make executable on Unix
338
+ if (process.platform !== "win32") {
339
+ try {
340
+ await fs.chmod(binPath, 0o755);
341
+ } catch {
342
+ /* ignore */
343
+ }
344
+ }
345
+
346
+ // NEW: Verify the binary actually works before returning
347
+ console.error(`[auto-install] Verifying ${binaryName}...`);
348
+ const isValid = await verifyToolBinary(binPath);
349
+ if (!isValid) {
350
+ console.error(
351
+ `[auto-install] ${packageName} installed but verification failed. The binary may be corrupted.`,
352
+ );
353
+ // Clean up the broken installation
354
+ try {
355
+ const packagePath = path.join(
356
+ TOOLS_DIR,
357
+ "node_modules",
358
+ packageName,
359
+ );
360
+ await fs.rm(packagePath, { recursive: true, force: true });
361
+ await fs.rm(binPath, { force: true });
362
+ if (isWindows) {
363
+ await fs.rm(`${binPath}.cmd`, { force: true });
364
+ await fs.rm(`${binPath}.ps1`, { force: true });
365
+ }
366
+ } catch {
367
+ /* ignore cleanup errors */
368
+ }
369
+ resolve(undefined);
370
+ return;
371
+ }
372
+
373
+ resolve(binPath);
374
+ } else {
375
+ reject(new Error(`Failed to install ${packageName}: ${stderr}`));
376
+ }
377
+ });
378
+
379
+ proc.on("error", (err) => reject(err));
380
+ });
381
+ } catch (err) {
382
+ console.error(
383
+ `[auto-install] Failed to install npm tool ${packageName}:`,
384
+ err,
385
+ );
386
+ return undefined;
387
+ }
388
+ }
389
+ /**
390
+ * Install a pip package tool
391
+ */
392
+ async function installPipTool(
393
+ packageName: string,
394
+ ): Promise<string | undefined> {
395
+ try {
396
+ const pipCmd = process.platform === "win32" ? "pip" : "pip3";
397
+ const isWindows = process.platform === "win32";
398
+ const proc = spawn(pipCmd, ["install", "--user", packageName], {
399
+ stdio: ["ignore", "pipe", "pipe"],
400
+ shell: isWindows, // Required for .cmd files on Windows
401
+ });
402
+
403
+ return new Promise((resolve, reject) => {
404
+ let stderr = "";
405
+ proc.stderr?.on("data", (data) => (stderr += data));
406
+
407
+ proc.on("exit", (code) => {
408
+ if (code === 0) {
409
+ resolve(packageName); // pip installs to PATH
410
+ } else {
411
+ reject(new Error(`Failed to install ${packageName}: ${stderr}`));
412
+ }
413
+ });
414
+
415
+ proc.on("error", (err) => reject(err));
416
+ });
417
+ } catch (err) {
418
+ console.error(
419
+ `[auto-install] Failed to install pip tool ${packageName}:`,
420
+ err,
421
+ );
422
+ return undefined;
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Install a tool by ID
428
+ */
429
+ export async function installTool(toolId: string): Promise<boolean> {
430
+ const tool = TOOLS.find((t) => t.id === toolId);
431
+ if (!tool) {
432
+ console.error(`[auto-install] Unknown tool: ${toolId}`);
433
+ return false;
434
+ }
435
+
436
+ console.error(`[auto-install] Installing ${tool.name}...`);
437
+
438
+ try {
439
+ switch (tool.installStrategy) {
440
+ case "npm": {
441
+ if (!tool.packageName || !tool.binaryName) return false;
442
+ const npmPath = await installNpmTool(tool.packageName, tool.binaryName);
443
+ return npmPath !== undefined;
444
+ }
445
+
446
+ case "pip": {
447
+ if (!tool.packageName) return false;
448
+ const pipPath = await installPipTool(tool.packageName);
449
+ return pipPath !== undefined;
450
+ }
451
+
452
+ default:
453
+ console.error(
454
+ `[auto-install] Unsupported strategy: ${tool.installStrategy}`,
455
+ );
456
+ return false;
457
+ }
458
+ } catch (err) {
459
+ console.error(`[auto-install] Failed to install ${tool.name}:`, err);
460
+ return false;
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Ensure a tool is installed (check first, install if missing)
466
+ */
467
+ export async function ensureTool(toolId: string): Promise<string | undefined> {
468
+ // Check if already installed
469
+ const existingPath = await getToolPath(toolId);
470
+ if (existingPath) {
471
+ return existingPath;
472
+ }
473
+
474
+ // Try to install
475
+ const installed = await installTool(toolId);
476
+ if (!installed) {
477
+ return undefined;
478
+ }
479
+
480
+ // Return the path after installation
481
+ return getToolPath(toolId);
482
+ }
483
+
484
+ // --- Integration Helpers ---
485
+
486
+ /**
487
+ * Get environment with tool paths added
488
+ */
489
+ export async function getToolEnvironment(): Promise<NodeJS.ProcessEnv> {
490
+ const localBin = path.join(TOOLS_DIR, "node_modules", ".bin");
491
+ const currentPath = process.env.PATH || "";
492
+ const separator = process.platform === "win32" ? ";" : ":";
493
+
494
+ return {
495
+ ...process.env,
496
+ PATH: `${localBin}${separator}${currentPath}`,
497
+ };
498
+ }
499
+
500
+ // --- Status Check ---
501
+
502
+ /**
503
+ * Check status of all managed tools
504
+ */
505
+ export async function checkAllTools(): Promise<
506
+ Array<{ id: string; name: string; installed: boolean; path?: string }>
507
+ > {
508
+ const results = [];
509
+ for (const tool of TOOLS) {
510
+ const path = await getToolPath(tool.id);
511
+ results.push({
512
+ id: tool.id,
513
+ name: tool.name,
514
+ installed: path !== undefined,
515
+ path,
516
+ });
517
+ }
518
+ return results;
519
+ }