pi-lens 2.2.9 → 3.0.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 (304) hide show
  1. package/CHANGELOG.md +198 -0
  2. package/README.md +709 -519
  3. package/clients/__tests__/file-time.test.js +216 -0
  4. package/clients/__tests__/file-time.test.ts +276 -0
  5. package/clients/__tests__/format-service.test.js +245 -0
  6. package/clients/__tests__/format-service.test.ts +339 -0
  7. package/clients/__tests__/formatters.test.js +271 -0
  8. package/clients/__tests__/formatters.test.ts +401 -0
  9. package/clients/amain-types.js +164 -0
  10. package/clients/amain-types.ts +165 -0
  11. package/clients/architect-client.js +56 -12
  12. package/clients/architect-client.ts +81 -16
  13. package/clients/ast-grep-client.js +2 -2
  14. package/clients/ast-grep-client.ts +14 -39
  15. package/clients/ast-grep-parser.ts +1 -1
  16. package/clients/ast-grep-rule-manager.js +8 -0
  17. package/clients/ast-grep-rule-manager.ts +10 -1
  18. package/clients/ast-grep-types.js +9 -0
  19. package/clients/ast-grep-types.ts +106 -0
  20. package/clients/auto-loop.js +10 -0
  21. package/clients/auto-loop.ts +14 -1
  22. package/clients/biome-client.js +81 -19
  23. package/clients/biome-client.ts +103 -22
  24. package/clients/bus/bus.js +191 -0
  25. package/clients/bus/bus.ts +251 -0
  26. package/clients/bus/events.js +214 -0
  27. package/clients/bus/events.ts +279 -0
  28. package/clients/bus/index.js +8 -0
  29. package/clients/bus/index.ts +9 -0
  30. package/clients/bus/integration.js +158 -0
  31. package/clients/bus/integration.ts +214 -0
  32. package/clients/complexity-client.js +13 -7
  33. package/clients/complexity-client.ts +13 -7
  34. package/clients/config-validator.js +465 -0
  35. package/clients/config-validator.ts +558 -0
  36. package/clients/dependency-checker.js +4 -10
  37. package/clients/dependency-checker.ts +4 -10
  38. package/clients/dispatch/__tests__/autofix-integration.test.js +245 -0
  39. package/clients/dispatch/__tests__/autofix-integration.test.ts +300 -0
  40. package/clients/dispatch/__tests__/runner-registration.test.js +236 -0
  41. package/clients/dispatch/__tests__/runner-registration.test.ts +282 -0
  42. package/clients/dispatch/bus-dispatcher.js +177 -0
  43. package/clients/dispatch/bus-dispatcher.ts +251 -0
  44. package/clients/dispatch/dispatcher.edge.test.js +82 -0
  45. package/clients/dispatch/dispatcher.edge.test.ts +100 -0
  46. package/clients/dispatch/dispatcher.format.test.js +46 -0
  47. package/clients/dispatch/dispatcher.format.test.ts +58 -0
  48. package/clients/dispatch/dispatcher.inline.test.js +74 -0
  49. package/clients/dispatch/dispatcher.inline.test.ts +93 -0
  50. package/clients/dispatch/dispatcher.js +19 -53
  51. package/clients/dispatch/dispatcher.ts +20 -67
  52. package/clients/dispatch/plan.js +9 -4
  53. package/clients/dispatch/plan.ts +9 -4
  54. package/clients/dispatch/runners/architect.js +21 -7
  55. package/clients/dispatch/runners/architect.test.js +138 -0
  56. package/clients/dispatch/runners/architect.test.ts +162 -0
  57. package/clients/dispatch/runners/architect.ts +22 -7
  58. package/clients/dispatch/runners/ast-grep-napi.js +462 -0
  59. package/clients/dispatch/runners/ast-grep-napi.test.js +111 -0
  60. package/clients/dispatch/runners/ast-grep-napi.test.ts +133 -0
  61. package/clients/dispatch/runners/ast-grep-napi.ts +506 -0
  62. package/clients/dispatch/runners/ast-grep.js +62 -19
  63. package/clients/dispatch/runners/ast-grep.ts +70 -18
  64. package/clients/dispatch/runners/biome.js +29 -53
  65. package/clients/dispatch/runners/biome.ts +29 -63
  66. package/clients/dispatch/runners/config-validation.js +67 -0
  67. package/clients/dispatch/runners/config-validation.ts +82 -0
  68. package/clients/dispatch/runners/go-vet.js +4 -28
  69. package/clients/dispatch/runners/go-vet.ts +4 -32
  70. package/clients/dispatch/runners/index.js +30 -10
  71. package/clients/dispatch/runners/index.ts +30 -10
  72. package/clients/dispatch/runners/oxlint.js +141 -0
  73. package/clients/dispatch/runners/oxlint.test.js +230 -0
  74. package/clients/dispatch/runners/oxlint.test.ts +303 -0
  75. package/clients/dispatch/runners/oxlint.ts +175 -0
  76. package/clients/dispatch/runners/pyright.js +40 -70
  77. package/clients/dispatch/runners/pyright.test.js +16 -2
  78. package/clients/dispatch/runners/pyright.test.ts +14 -2
  79. package/clients/dispatch/runners/pyright.ts +48 -91
  80. package/clients/dispatch/runners/python-slop.js +97 -0
  81. package/clients/dispatch/runners/python-slop.test.js +203 -0
  82. package/clients/dispatch/runners/python-slop.test.ts +298 -0
  83. package/clients/dispatch/runners/python-slop.ts +124 -0
  84. package/clients/dispatch/runners/ruff.js +18 -71
  85. package/clients/dispatch/runners/ruff.ts +19 -79
  86. package/clients/dispatch/runners/rust-clippy.js +28 -32
  87. package/clients/dispatch/runners/rust-clippy.ts +29 -31
  88. package/clients/dispatch/runners/scan_codebase.test.js +89 -0
  89. package/clients/dispatch/runners/scan_codebase.test.ts +105 -0
  90. package/clients/dispatch/runners/shellcheck.js +147 -0
  91. package/clients/dispatch/runners/shellcheck.test.js +98 -0
  92. package/clients/dispatch/runners/shellcheck.test.ts +129 -0
  93. package/clients/dispatch/runners/shellcheck.ts +188 -0
  94. package/clients/dispatch/runners/similarity.js +230 -0
  95. package/clients/dispatch/runners/similarity.ts +339 -0
  96. package/clients/dispatch/runners/spellcheck.js +106 -0
  97. package/clients/dispatch/runners/spellcheck.test.js +158 -0
  98. package/clients/dispatch/runners/spellcheck.test.ts +214 -0
  99. package/clients/dispatch/runners/spellcheck.ts +136 -0
  100. package/clients/dispatch/runners/tree-sitter.js +107 -0
  101. package/clients/dispatch/runners/tree-sitter.ts +135 -0
  102. package/clients/dispatch/runners/ts-lsp.js +104 -33
  103. package/clients/dispatch/runners/ts-lsp.ts +120 -38
  104. package/clients/dispatch/runners/ts-slop.js +113 -0
  105. package/clients/dispatch/runners/ts-slop.test.js +180 -0
  106. package/clients/dispatch/runners/ts-slop.test.ts +230 -0
  107. package/clients/dispatch/runners/ts-slop.ts +142 -0
  108. package/clients/dispatch/runners/utils/diagnostic-parsers.js +134 -0
  109. package/clients/dispatch/runners/utils/diagnostic-parsers.ts +186 -0
  110. package/clients/dispatch/runners/utils/runner-helpers.js +115 -0
  111. package/clients/dispatch/runners/utils/runner-helpers.ts +167 -0
  112. package/clients/dispatch/runners/utils.js +2 -4
  113. package/clients/dispatch/runners/utils.ts +2 -4
  114. package/clients/dispatch/types.ts +1 -1
  115. package/clients/dispatch/utils/format-utils.js +49 -0
  116. package/clients/dispatch/utils/format-utils.ts +60 -0
  117. package/clients/dogfood.test.js +201 -0
  118. package/clients/dogfood.test.ts +269 -0
  119. package/clients/file-time.js +152 -0
  120. package/clients/file-time.ts +208 -0
  121. package/clients/file-utils.js +40 -0
  122. package/clients/file-utils.ts +44 -0
  123. package/clients/fix-scanners.js +10 -20
  124. package/clients/fix-scanners.ts +10 -22
  125. package/clients/format-service.js +172 -0
  126. package/clients/format-service.ts +254 -0
  127. package/clients/formatters.js +435 -0
  128. package/clients/formatters.ts +508 -0
  129. package/clients/go-client.js +5 -14
  130. package/clients/go-client.ts +5 -13
  131. package/clients/installer/index.js +356 -0
  132. package/clients/installer/index.ts +426 -0
  133. package/clients/jscpd-client.js +11 -9
  134. package/clients/jscpd-client.ts +12 -8
  135. package/clients/knip-client.js +3 -7
  136. package/clients/knip-client.ts +3 -6
  137. package/clients/lsp/__tests__/client.test.js +325 -0
  138. package/clients/lsp/__tests__/client.test.ts +434 -0
  139. package/clients/lsp/__tests__/config.test.js +166 -0
  140. package/clients/lsp/__tests__/config.test.ts +209 -0
  141. package/clients/lsp/__tests__/error-recovery.test.js +213 -0
  142. package/clients/lsp/__tests__/error-recovery.test.ts +279 -0
  143. package/clients/lsp/__tests__/integration.test.js +127 -0
  144. package/clients/lsp/__tests__/integration.test.ts +160 -0
  145. package/clients/lsp/__tests__/launch.test.js +260 -0
  146. package/clients/lsp/__tests__/launch.test.ts +329 -0
  147. package/clients/lsp/__tests__/server.test.js +259 -0
  148. package/clients/lsp/__tests__/server.test.ts +332 -0
  149. package/clients/lsp/__tests__/service.test.js +417 -0
  150. package/clients/lsp/__tests__/service.test.ts +499 -0
  151. package/clients/lsp/client.js +235 -0
  152. package/clients/lsp/client.ts +328 -0
  153. package/clients/lsp/config.js +115 -0
  154. package/clients/lsp/config.ts +149 -0
  155. package/clients/lsp/index.js +222 -0
  156. package/clients/lsp/index.ts +280 -0
  157. package/clients/lsp/installer/index.js +391 -0
  158. package/clients/lsp/interactive-install.js +210 -0
  159. package/clients/lsp/interactive-install.ts +251 -0
  160. package/clients/lsp/language.js +170 -0
  161. package/clients/lsp/language.ts +216 -0
  162. package/clients/lsp/launch.js +174 -0
  163. package/clients/lsp/launch.ts +240 -0
  164. package/clients/lsp/lsp/launch.js +116 -0
  165. package/clients/lsp/lsp/server.js +532 -0
  166. package/clients/lsp/lsp-index.js +10 -0
  167. package/clients/lsp/lsp-index.ts +11 -0
  168. package/clients/lsp/path-utils.js +48 -0
  169. package/clients/lsp/path-utils.ts +52 -0
  170. package/clients/lsp/server.js +615 -0
  171. package/clients/lsp/server.ts +800 -0
  172. package/clients/lsp/test-py-spawn/requirements.txt +1 -0
  173. package/clients/lsp/test-py-spawn/test.py +3 -0
  174. package/clients/lsp/test-py-svc/requirements.txt +1 -0
  175. package/clients/lsp/test-py-svc/test.py +3 -0
  176. package/clients/lsp/test-python-project/requirements.txt +1 -0
  177. package/clients/lsp/test-python-project/test.py +5 -0
  178. package/clients/metrics-history.js +2 -2
  179. package/clients/metrics-history.ts +2 -2
  180. package/clients/production-readiness.js +522 -0
  181. package/clients/production-readiness.ts +556 -0
  182. package/clients/project-index.js +255 -0
  183. package/clients/project-index.ts +383 -0
  184. package/clients/project-metadata.js +531 -0
  185. package/clients/project-metadata.ts +624 -0
  186. package/clients/ruff-client.js +56 -16
  187. package/clients/ruff-client.ts +72 -15
  188. package/clients/runner-tracker.js +152 -0
  189. package/clients/runner-tracker.ts +213 -0
  190. package/clients/rust-client.js +4 -11
  191. package/clients/rust-client.ts +5 -11
  192. package/clients/safe-spawn.js +96 -0
  193. package/clients/safe-spawn.ts +128 -0
  194. package/clients/scan-architectural-debt.js +3 -6
  195. package/clients/scan-architectural-debt.ts +3 -6
  196. package/clients/scan-utils.js +5 -20
  197. package/clients/scan-utils.ts +5 -29
  198. package/clients/secrets-scanner.js +3 -17
  199. package/clients/secrets-scanner.ts +4 -20
  200. package/clients/services/__tests__/effect-integration.test.js +86 -0
  201. package/clients/services/__tests__/effect-integration.test.ts +111 -0
  202. package/clients/services/effect-integration.js +194 -0
  203. package/clients/services/effect-integration.ts +268 -0
  204. package/clients/services/index.js +7 -0
  205. package/clients/services/index.ts +8 -0
  206. package/clients/services/runner-service.js +105 -0
  207. package/clients/services/runner-service.ts +179 -0
  208. package/clients/sg-runner.js +87 -13
  209. package/clients/sg-runner.ts +97 -13
  210. package/clients/state-matrix.js +160 -0
  211. package/clients/state-matrix.ts +202 -0
  212. package/clients/subprocess-client.js +10 -9
  213. package/clients/subprocess-client.ts +10 -8
  214. package/clients/test-runner-client.js +3 -7
  215. package/clients/test-runner-client.ts +3 -6
  216. package/clients/tool-availability.js +4 -10
  217. package/clients/tool-availability.ts +4 -9
  218. package/clients/tree-sitter-client.js +564 -0
  219. package/clients/tree-sitter-client.ts +797 -0
  220. package/clients/tree-sitter-query-loader.js +355 -0
  221. package/clients/tree-sitter-query-loader.ts +425 -0
  222. package/clients/type-coverage-client.js +3 -7
  223. package/clients/type-coverage-client.ts +3 -6
  224. package/clients/typescript-client.codefix.test.js +157 -0
  225. package/clients/typescript-client.codefix.test.ts +186 -0
  226. package/clients/typescript-client.js +43 -0
  227. package/clients/typescript-client.ts +98 -0
  228. package/commands/booboo.js +799 -219
  229. package/commands/booboo.ts +1004 -225
  230. package/commands/clients/ast-grep-client.js +250 -0
  231. package/commands/clients/ast-grep-parser.js +86 -0
  232. package/commands/clients/ast-grep-rule-manager.js +91 -0
  233. package/commands/clients/ast-grep-types.js +9 -0
  234. package/commands/clients/biome-client.js +380 -0
  235. package/commands/clients/complexity-client.js +667 -0
  236. package/commands/clients/file-kinds.js +177 -0
  237. package/commands/clients/file-utils.js +40 -0
  238. package/commands/clients/jscpd-client.js +169 -0
  239. package/commands/clients/knip-client.js +211 -0
  240. package/commands/clients/ruff-client.js +297 -0
  241. package/commands/clients/safe-spawn.js +88 -0
  242. package/commands/clients/scan-utils.js +83 -0
  243. package/commands/clients/sg-runner.js +190 -0
  244. package/commands/clients/types.js +11 -0
  245. package/commands/clients/typescript-client.js +505 -0
  246. package/commands/fix-from-booboo.js +398 -0
  247. package/commands/fix-from-booboo.ts +485 -0
  248. package/commands/fix-simplified.js +618 -0
  249. package/commands/fix-simplified.ts +768 -0
  250. package/commands/rate.js +10 -14
  251. package/commands/rate.ts +9 -16
  252. package/default-architect.yaml +59 -15
  253. package/index.ts +342 -429
  254. package/package.json +16 -3
  255. package/rules/ast-grep-rules/rules/empty-catch.yml +38 -13
  256. package/rules/ast-grep-rules/rules/no-array-constructor.yml +1 -0
  257. package/rules/ast-grep-rules/rules/no-debugger.yml +2 -0
  258. package/rules/python-slop-rules/.sgconfig.yml +4 -0
  259. package/rules/python-slop-rules/rules/slop-rules.yml +647 -0
  260. package/rules/tree-sitter-queries/python/bare-except.yml +54 -0
  261. package/rules/tree-sitter-queries/python/eval-exec.yml +50 -0
  262. package/rules/tree-sitter-queries/python/is-vs-equals.yml +60 -0
  263. package/rules/tree-sitter-queries/python/mutable-default-arg.yml +57 -0
  264. package/rules/tree-sitter-queries/python/unreachable-except.yml +60 -0
  265. package/rules/tree-sitter-queries/python/wildcard-import.yml +46 -0
  266. package/rules/tree-sitter-queries/tsx/dangerously-set-inner-html.yml +63 -0
  267. package/rules/tree-sitter-queries/typescript/await-in-loop.yml +56 -0
  268. package/rules/tree-sitter-queries/typescript/console-statement.yml +47 -0
  269. package/rules/tree-sitter-queries/typescript/debugger.yml +47 -0
  270. package/rules/tree-sitter-queries/typescript/deep-nesting.yml +117 -0
  271. package/rules/tree-sitter-queries/typescript/deep-promise-chain.yml +73 -0
  272. package/rules/tree-sitter-queries/typescript/empty-catch.yml +64 -0
  273. package/rules/tree-sitter-queries/typescript/eval.yml +48 -0
  274. package/rules/tree-sitter-queries/typescript/hardcoded-secrets.yml +78 -0
  275. package/rules/tree-sitter-queries/typescript/long-parameter-list.yml +62 -0
  276. package/rules/tree-sitter-queries/typescript/mixed-async-styles.yml +49 -0
  277. package/rules/tree-sitter-queries/typescript/nested-ternary.yml +45 -0
  278. package/rules/ts-slop-rules/.sgconfig.yml +4 -0
  279. package/rules/ts-slop-rules/rules/in-correct-optional-input-type.yml +10 -0
  280. package/rules/ts-slop-rules/rules/jwt-no-verify.yml +13 -0
  281. package/rules/ts-slop-rules/rules/no-architecture-violation.yml +10 -0
  282. package/rules/ts-slop-rules/rules/no-case-declarations.yml +10 -0
  283. package/rules/ts-slop-rules/rules/no-dangerously-set-inner-html.yml +10 -0
  284. package/rules/ts-slop-rules/rules/no-debugger.yml +10 -0
  285. package/rules/ts-slop-rules/rules/no-dupe-args.yml +10 -0
  286. package/rules/ts-slop-rules/rules/no-dupe-class-members.yml +10 -0
  287. package/rules/ts-slop-rules/rules/no-dupe-keys.yml +10 -0
  288. package/rules/ts-slop-rules/rules/no-eval.yml +13 -0
  289. package/rules/ts-slop-rules/rules/no-hardcoded-secrets.yml +12 -0
  290. package/rules/ts-slop-rules/rules/no-implied-eval.yml +12 -0
  291. package/rules/ts-slop-rules/rules/no-inner-html.yml +13 -0
  292. package/rules/ts-slop-rules/rules/no-javascript-url.yml +10 -0
  293. package/rules/ts-slop-rules/rules/no-mutable-default.yml +10 -0
  294. package/rules/ts-slop-rules/rules/no-nested-links.yml +12 -0
  295. package/rules/ts-slop-rules/rules/no-new-symbol.yml +10 -0
  296. package/rules/ts-slop-rules/rules/no-new-wrappers.yml +13 -0
  297. package/rules/ts-slop-rules/rules/no-open-redirect.yml +16 -0
  298. package/rules/ts-slop-rules/rules/slop-rules.yml +455 -0
  299. package/rules/ts-slop-rules/rules/weak-rsa-key.yml +12 -0
  300. package/skills/ast-grep/SKILL.md +182 -0
  301. package/clients/dispatch/runners/secrets.js +0 -109
  302. package/commands/fix.js +0 -244
  303. package/commands/fix.ts +0 -373
  304. package/rules/ast-grep-rules/rules/no-lonely-if.yml +0 -13
@@ -0,0 +1,149 @@
1
+ /**
2
+ * LSP Configuration for pi-lens
3
+ *
4
+ * Allows users to define custom LSP servers via configuration.
5
+ *
6
+ * Config file: .pi-lens/lsp.json
7
+ *
8
+ * Example:
9
+ * {
10
+ * "servers": {
11
+ * "my-server": {
12
+ * "name": "My Custom LSP",
13
+ * "extensions": [".myext"],
14
+ * "command": "my-lsp-server",
15
+ * "args": ["--stdio"],
16
+ * "rootMarkers": ["package.json"]
17
+ * }
18
+ * }
19
+ * }
20
+ */
21
+
22
+ import fs from "fs/promises";
23
+ import path from "path";
24
+ import { fileURLToPath } from "url";
25
+ import { LSP_SERVERS, type LSPServerInfo, createRootDetector } from "./server.js";
26
+ import { launchLSP } from "./launch.js";
27
+
28
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
29
+
30
+ // --- Types ---
31
+
32
+ export interface CustomServerConfig {
33
+ name: string;
34
+ extensions: string[];
35
+ command: string;
36
+ args?: string[];
37
+ rootMarkers?: string[];
38
+ env?: Record<string, string>;
39
+ }
40
+
41
+ export interface LSPConfig {
42
+ servers?: Record<string, CustomServerConfig>;
43
+ disabledServers?: string[];
44
+ }
45
+
46
+ // --- Config Loading ---
47
+
48
+ const CONFIG_PATHS = [
49
+ ".pi-lens/lsp.json",
50
+ ".pi-lens.json",
51
+ "pi-lsp.json",
52
+ ];
53
+
54
+ /**
55
+ * Load LSP configuration from file
56
+ */
57
+ export async function loadLSPConfig(cwd: string): Promise<LSPConfig> {
58
+ for (const configPath of CONFIG_PATHS) {
59
+ const fullPath = path.join(cwd, configPath);
60
+ try {
61
+ const content = await fs.readFile(fullPath, "utf-8");
62
+ const config = JSON.parse(content) as LSPConfig;
63
+ console.error(`[lsp-config] Loaded config from ${configPath}`);
64
+ return config;
65
+ } catch {
66
+ // File doesn't exist or is invalid, try next
67
+ }
68
+ }
69
+ return {};
70
+ }
71
+
72
+ // --- Custom Server Factory ---
73
+
74
+ /**
75
+ * Create LSPServerInfo from user configuration
76
+ */
77
+ export function createCustomServer(config: CustomServerConfig, id: string): LSPServerInfo {
78
+ return {
79
+ id,
80
+ name: config.name,
81
+ extensions: config.extensions,
82
+ root: config.rootMarkers
83
+ ? createRootDetector(config.rootMarkers)
84
+ : async () => process.cwd(),
85
+ async spawn(root) {
86
+ const proc = launchLSP(config.command, config.args ?? ["--stdio"], {
87
+ cwd: root,
88
+ env: config.env ? { ...process.env, ...config.env } : process.env,
89
+ });
90
+ return { process: proc };
91
+ },
92
+ };
93
+ }
94
+
95
+ // --- Registry Management ---
96
+
97
+ let customServers: LSPServerInfo[] = [];
98
+ let disabledServerIds: Set<string> = new Set();
99
+
100
+ /**
101
+ * Initialize LSP configuration (call at session start)
102
+ */
103
+ export async function initLSPConfig(cwd: string): Promise<void> {
104
+ const config = await loadLSPConfig(cwd);
105
+
106
+ // Clear previous custom servers
107
+ customServers = [];
108
+ disabledServerIds = new Set(config.disabledServers ?? []);
109
+
110
+ // Register custom servers from config
111
+ if (config.servers) {
112
+ for (const [id, serverConfig] of Object.entries(config.servers)) {
113
+ try {
114
+ const server = createCustomServer(serverConfig, id);
115
+ customServers.push(server);
116
+ console.error(`[lsp-config] Registered custom server: ${id} (${serverConfig.name})`);
117
+ } catch (err) {
118
+ console.error(`[lsp-config] Failed to register server ${id}:`, err);
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Get all available servers (built-in + custom, minus disabled)
126
+ */
127
+ export function getAllServers(): LSPServerInfo[] {
128
+ const all = [...LSP_SERVERS, ...customServers];
129
+ return all.filter(s => !disabledServerIds.has(s.id));
130
+ }
131
+
132
+ /**
133
+ * Check if a server is disabled
134
+ */
135
+ export function isServerDisabled(serverId: string): boolean {
136
+ return disabledServerIds.has(serverId);
137
+ }
138
+
139
+ // --- Override getServersForFile to include custom servers
140
+
141
+ import { getServersForFile as getBuiltinServersForFile } from "./server.js";
142
+
143
+ export function getServersForFileWithConfig(filePath: string): LSPServerInfo[] {
144
+ const ext = path.extname(filePath).toLowerCase();
145
+ return getAllServers().filter((server) => server.extensions.includes(ext));
146
+ }
147
+
148
+ // Re-export with config support
149
+ export { getAllServers as getServersForFile };
@@ -0,0 +1,222 @@
1
+ /**
2
+ * LSP Service Layer for pi-lens
3
+ *
4
+ * Manages multiple LSP clients per workspace with:
5
+ * - Auto-spawning based on file type
6
+ * - Effect-TS service composition
7
+ * - Bus event integration
8
+ * - Resource cleanup
9
+ */
10
+ import { Effect } from "effect";
11
+ import { createLSPClient } from "./client.js";
12
+ import { getServersForFileWithConfig } from "./config.js";
13
+ import { getLanguageId } from "./language.js";
14
+ // --- Service ---
15
+ export class LSPService {
16
+ constructor() {
17
+ this.state = {
18
+ clients: new Map(),
19
+ servers: new Map(),
20
+ broken: new Set(),
21
+ inFlight: new Map(),
22
+ };
23
+ }
24
+ /**
25
+ * Get or create LSP client for a file
26
+ * Prevents duplicate client creation via in-flight promise tracking
27
+ */
28
+ async getClientForFile(filePath) {
29
+ const servers = getServersForFileWithConfig(filePath);
30
+ if (servers.length === 0)
31
+ return undefined;
32
+ // Try each matching server
33
+ for (const server of servers) {
34
+ const root = await server.root(filePath);
35
+ if (!root)
36
+ continue;
37
+ // Normalize root path for consistent cache key on Windows
38
+ const normalizedRoot = process.platform === "win32" ? root.toLowerCase() : root;
39
+ const key = `${server.id}:${normalizedRoot}`;
40
+ // Check cache first (fast path)
41
+ const existing = this.state.clients.get(key);
42
+ if (existing) {
43
+ return { client: existing, info: server };
44
+ }
45
+ // Check if broken
46
+ if (this.state.broken.has(key)) {
47
+ continue;
48
+ }
49
+ // Check if there's already an in-flight spawn for this key
50
+ const inFlight = this.state.inFlight.get(key);
51
+ if (inFlight) {
52
+ // Wait for the existing spawn to complete
53
+ const result = await inFlight;
54
+ if (result)
55
+ return result;
56
+ continue; // This server failed, try next
57
+ }
58
+ // Create the spawn promise and store it
59
+ const spawnPromise = this.spawnClient(server, root, key);
60
+ this.state.inFlight.set(key, spawnPromise);
61
+ try {
62
+ const result = await spawnPromise;
63
+ if (result)
64
+ return result;
65
+ }
66
+ finally {
67
+ // Clean up in-flight tracking
68
+ this.state.inFlight.delete(key);
69
+ }
70
+ }
71
+ return undefined;
72
+ }
73
+ /**
74
+ * Internal: spawn a client for a server/root combination
75
+ */
76
+ async spawnClient(server, root, key) {
77
+ try {
78
+ const spawned = await server.spawn(root);
79
+ if (!spawned) {
80
+ this.state.broken.add(key);
81
+ return undefined;
82
+ }
83
+ const client = await createLSPClient({
84
+ serverId: server.id,
85
+ process: spawned.process,
86
+ root,
87
+ initialization: spawned.initialization,
88
+ });
89
+ this.state.clients.set(key, client);
90
+ return { client, info: server };
91
+ }
92
+ catch (err) {
93
+ const errorMsg = err instanceof Error ? err.message : String(err);
94
+ if (errorMsg.includes("Timeout")) {
95
+ console.error(`[lsp] ${server.id} timed out during initialization (${errorMsg}). The server may be downloading or the project is large. Skipping.`);
96
+ }
97
+ else if (errorMsg.includes("stream was destroyed")) {
98
+ console.error(`[lsp] ${server.id} stream was destroyed. The server binary may be missing or crashed immediately. Try reinstalling: npm install -g ${server.id}-language-server`);
99
+ }
100
+ else if (errorMsg.includes("exited immediately")) {
101
+ console.error(`[lsp] ${server.id} ${errorMsg}. Try reinstalling: npm install -g ${server.id}-language-server`);
102
+ }
103
+ else {
104
+ console.error(`[lsp] Failed to spawn ${server.id}:`, err);
105
+ }
106
+ this.state.broken.add(key);
107
+ return undefined;
108
+ }
109
+ }
110
+ /**
111
+ * Open a file in LSP (sends textDocument/didOpen)
112
+ */
113
+ async openFile(filePath, content) {
114
+ const spawned = await this.getClientForFile(filePath);
115
+ if (!spawned)
116
+ return;
117
+ const languageId = getLanguageId(filePath) ?? "plaintext";
118
+ await spawned.client.notify.open(filePath, content, languageId);
119
+ }
120
+ /**
121
+ * Update file content (sends textDocument/didChange)
122
+ */
123
+ async updateFile(filePath, content) {
124
+ const spawned = await this.getClientForFile(filePath);
125
+ if (!spawned)
126
+ return;
127
+ await spawned.client.notify.change(filePath, content);
128
+ }
129
+ /**
130
+ * Get diagnostics for a file
131
+ */
132
+ async getDiagnostics(filePath) {
133
+ const spawned = await this.getClientForFile(filePath);
134
+ if (!spawned)
135
+ return [];
136
+ await spawned.client.waitForDiagnostics(filePath, 3000);
137
+ return spawned.client.getDiagnostics(filePath);
138
+ }
139
+ /**
140
+ * Check if LSP is available for a file
141
+ */
142
+ async hasLSP(filePath) {
143
+ const servers = getServersForFileWithConfig(filePath);
144
+ if (servers.length === 0)
145
+ return false;
146
+ // Check if any server can provide a root
147
+ for (const server of servers) {
148
+ const root = await server.root(filePath);
149
+ if (root)
150
+ return true;
151
+ }
152
+ return false;
153
+ }
154
+ /**
155
+ * Shutdown all LSP clients
156
+ */
157
+ async shutdown() {
158
+ // Cancel any in-flight spawns
159
+ this.state.inFlight.clear();
160
+ for (const [key, client] of this.state.clients) {
161
+ try {
162
+ await client.shutdown();
163
+ }
164
+ catch (err) {
165
+ console.error(`[lsp] Error shutting down ${key}:`, err);
166
+ }
167
+ }
168
+ this.state.clients.clear();
169
+ this.state.broken.clear();
170
+ }
171
+ /**
172
+ * Get status of all active clients
173
+ */
174
+ getStatus() {
175
+ return Array.from(this.state.clients.entries()).map(([key, _client]) => {
176
+ const [serverId, root] = key.split(":");
177
+ return { serverId, root, connected: true };
178
+ });
179
+ }
180
+ }
181
+ // --- Effect Integration ---
182
+ /**
183
+ * Effect wrapper for LSP operations
184
+ */
185
+ export function lspEffect(service) {
186
+ return {
187
+ openFile: (filePath, content) => Effect.tryPromise({
188
+ try: () => service.openFile(filePath, content),
189
+ catch: (err) => err,
190
+ }),
191
+ updateFile: (filePath, content) => Effect.tryPromise({
192
+ try: () => service.updateFile(filePath, content),
193
+ catch: (err) => err,
194
+ }),
195
+ getDiagnostics: (filePath) => Effect.tryPromise({
196
+ try: () => service.getDiagnostics(filePath),
197
+ catch: (err) => err,
198
+ }),
199
+ hasLSP: (filePath) => Effect.tryPromise({
200
+ try: () => service.hasLSP(filePath),
201
+ catch: (err) => err,
202
+ }),
203
+ shutdown: () => Effect.tryPromise({
204
+ try: () => service.shutdown(),
205
+ catch: (err) => err,
206
+ }),
207
+ };
208
+ }
209
+ // --- Singleton Instance ---
210
+ let globalLSPService = null;
211
+ export function getLSPService() {
212
+ if (!globalLSPService) {
213
+ globalLSPService = new LSPService();
214
+ }
215
+ return globalLSPService;
216
+ }
217
+ export function resetLSPService() {
218
+ if (globalLSPService) {
219
+ globalLSPService.shutdown().catch(() => { });
220
+ }
221
+ globalLSPService = null;
222
+ }
@@ -0,0 +1,280 @@
1
+ /**
2
+ * LSP Service Layer for pi-lens
3
+ *
4
+ * Manages multiple LSP clients per workspace with:
5
+ * - Auto-spawning based on file type
6
+ * - Effect-TS service composition
7
+ * - Bus event integration
8
+ * - Resource cleanup
9
+ */
10
+
11
+ import { Effect } from "effect";
12
+ import type { LSPClientInfo } from "./client.js";
13
+ import { createLSPClient } from "./client.js";
14
+ import { getServersForFileWithConfig } from "./config.js";
15
+ import { getLanguageId } from "./language.js";
16
+ import type { LSPServerInfo } from "./server.js";
17
+
18
+ // --- Types ---
19
+
20
+ export interface LSPState {
21
+ clients: Map<string, LSPClientInfo>; // key: "serverId:root"
22
+ servers: Map<string, LSPServerInfo>;
23
+ broken: Set<string>; // servers that failed to initialize
24
+ inFlight: Map<string, Promise<SpawnedServer | undefined>>; // prevent duplicate spawns
25
+ }
26
+
27
+ export interface SpawnedServer {
28
+ client: LSPClientInfo;
29
+ info: LSPServerInfo;
30
+ }
31
+
32
+ // --- Service ---
33
+
34
+ export class LSPService {
35
+ private state: LSPState;
36
+
37
+ constructor() {
38
+ this.state = {
39
+ clients: new Map(),
40
+ servers: new Map(),
41
+ broken: new Set(),
42
+ inFlight: new Map(),
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Get or create LSP client for a file
48
+ * Prevents duplicate client creation via in-flight promise tracking
49
+ */
50
+ async getClientForFile(filePath: string): Promise<SpawnedServer | undefined> {
51
+ const servers = getServersForFileWithConfig(filePath);
52
+ if (servers.length === 0) return undefined;
53
+
54
+ // Try each matching server
55
+ for (const server of servers) {
56
+ const root = await server.root(filePath);
57
+ if (!root) continue;
58
+
59
+ // Normalize root path for consistent cache key on Windows
60
+ const normalizedRoot =
61
+ process.platform === "win32" ? root.toLowerCase() : root;
62
+ const key = `${server.id}:${normalizedRoot}`;
63
+
64
+ // Check cache first (fast path)
65
+ const existing = this.state.clients.get(key);
66
+ if (existing) {
67
+ return { client: existing, info: server };
68
+ }
69
+
70
+ // Check if broken
71
+ if (this.state.broken.has(key)) {
72
+ continue;
73
+ }
74
+
75
+ // Check if there's already an in-flight spawn for this key
76
+ const inFlight = this.state.inFlight.get(key);
77
+ if (inFlight) {
78
+ // Wait for the existing spawn to complete
79
+ const result = await inFlight;
80
+ if (result) return result;
81
+ continue; // This server failed, try next
82
+ }
83
+
84
+ // Create the spawn promise and store it
85
+ const spawnPromise = this.spawnClient(server, root, key);
86
+ this.state.inFlight.set(key, spawnPromise);
87
+
88
+ try {
89
+ const result = await spawnPromise;
90
+ if (result) return result;
91
+ } finally {
92
+ // Clean up in-flight tracking
93
+ this.state.inFlight.delete(key);
94
+ }
95
+ }
96
+
97
+ return undefined;
98
+ }
99
+
100
+ /**
101
+ * Internal: spawn a client for a server/root combination
102
+ */
103
+ private async spawnClient(
104
+ server: LSPServerInfo,
105
+ root: string,
106
+ key: string,
107
+ ): Promise<SpawnedServer | undefined> {
108
+ try {
109
+ const spawned = await server.spawn(root);
110
+ if (!spawned) {
111
+ this.state.broken.add(key);
112
+ return undefined;
113
+ }
114
+
115
+ const client = await createLSPClient({
116
+ serverId: server.id,
117
+ process: spawned.process,
118
+ root,
119
+ initialization: spawned.initialization,
120
+ });
121
+
122
+ this.state.clients.set(key, client);
123
+ return { client, info: server };
124
+ } catch (err) {
125
+ const errorMsg = err instanceof Error ? err.message : String(err);
126
+ if (errorMsg.includes("Timeout")) {
127
+ console.error(
128
+ `[lsp] ${server.id} timed out during initialization (${errorMsg}). The server may be downloading or the project is large. Skipping.`,
129
+ );
130
+ } else if (errorMsg.includes("stream was destroyed")) {
131
+ console.error(
132
+ `[lsp] ${server.id} stream was destroyed. The server binary may be missing or crashed immediately. Try reinstalling: npm install -g ${server.id}-language-server`,
133
+ );
134
+ } else if (errorMsg.includes("exited immediately")) {
135
+ console.error(
136
+ `[lsp] ${server.id} ${errorMsg}. Try reinstalling: npm install -g ${server.id}-language-server`,
137
+ );
138
+ } else {
139
+ console.error(`[lsp] Failed to spawn ${server.id}:`, err);
140
+ }
141
+ this.state.broken.add(key);
142
+ return undefined;
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Open a file in LSP (sends textDocument/didOpen)
148
+ */
149
+ async openFile(filePath: string, content: string): Promise<void> {
150
+ const spawned = await this.getClientForFile(filePath);
151
+ if (!spawned) return;
152
+
153
+ const languageId = getLanguageId(filePath) ?? "plaintext";
154
+ await spawned.client.notify.open(filePath, content, languageId);
155
+ }
156
+
157
+ /**
158
+ * Update file content (sends textDocument/didChange)
159
+ */
160
+ async updateFile(filePath: string, content: string): Promise<void> {
161
+ const spawned = await this.getClientForFile(filePath);
162
+ if (!spawned) return;
163
+
164
+ await spawned.client.notify.change(filePath, content);
165
+ }
166
+
167
+ /**
168
+ * Get diagnostics for a file
169
+ */
170
+ async getDiagnostics(
171
+ filePath: string,
172
+ ): Promise<import("./client.js").LSPDiagnostic[]> {
173
+ const spawned = await this.getClientForFile(filePath);
174
+ if (!spawned) return [];
175
+
176
+ await spawned.client.waitForDiagnostics(filePath, 3000);
177
+ return spawned.client.getDiagnostics(filePath);
178
+ }
179
+
180
+ /**
181
+ * Check if LSP is available for a file
182
+ */
183
+ async hasLSP(filePath: string): Promise<boolean> {
184
+ const servers = getServersForFileWithConfig(filePath);
185
+ if (servers.length === 0) return false;
186
+
187
+ // Check if any server can provide a root
188
+ for (const server of servers) {
189
+ const root = await server.root(filePath);
190
+ if (root) return true;
191
+ }
192
+
193
+ return false;
194
+ }
195
+
196
+ /**
197
+ * Shutdown all LSP clients
198
+ */
199
+ async shutdown(): Promise<void> {
200
+ // Cancel any in-flight spawns
201
+ this.state.inFlight.clear();
202
+
203
+ for (const [key, client] of this.state.clients) {
204
+ try {
205
+ await client.shutdown();
206
+ } catch (err) {
207
+ console.error(`[lsp] Error shutting down ${key}:`, err);
208
+ }
209
+ }
210
+ this.state.clients.clear();
211
+ this.state.broken.clear();
212
+ }
213
+
214
+ /**
215
+ * Get status of all active clients
216
+ */
217
+ getStatus(): Array<{ serverId: string; root: string; connected: boolean }> {
218
+ return Array.from(this.state.clients.entries()).map(([key, _client]) => {
219
+ const [serverId, root] = key.split(":");
220
+ return { serverId, root, connected: true };
221
+ });
222
+ }
223
+ }
224
+
225
+ // --- Effect Integration ---
226
+
227
+ /**
228
+ * Effect wrapper for LSP operations
229
+ */
230
+ export function lspEffect(service: LSPService) {
231
+ return {
232
+ openFile: (filePath: string, content: string) =>
233
+ Effect.tryPromise({
234
+ try: () => service.openFile(filePath, content),
235
+ catch: (err) => err as Error,
236
+ }),
237
+
238
+ updateFile: (filePath: string, content: string) =>
239
+ Effect.tryPromise({
240
+ try: () => service.updateFile(filePath, content),
241
+ catch: (err) => err as Error,
242
+ }),
243
+
244
+ getDiagnostics: (filePath: string) =>
245
+ Effect.tryPromise({
246
+ try: () => service.getDiagnostics(filePath),
247
+ catch: (err) => err as Error,
248
+ }),
249
+
250
+ hasLSP: (filePath: string) =>
251
+ Effect.tryPromise({
252
+ try: () => service.hasLSP(filePath),
253
+ catch: (err) => err as Error,
254
+ }),
255
+
256
+ shutdown: () =>
257
+ Effect.tryPromise({
258
+ try: () => service.shutdown(),
259
+ catch: (err) => err as Error,
260
+ }),
261
+ };
262
+ }
263
+
264
+ // --- Singleton Instance ---
265
+
266
+ let globalLSPService: LSPService | null = null;
267
+
268
+ export function getLSPService(): LSPService {
269
+ if (!globalLSPService) {
270
+ globalLSPService = new LSPService();
271
+ }
272
+ return globalLSPService;
273
+ }
274
+
275
+ export function resetLSPService(): void {
276
+ if (globalLSPService) {
277
+ globalLSPService.shutdown().catch(() => {});
278
+ }
279
+ globalLSPService = null;
280
+ }