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,174 @@
1
+ /**
2
+ * LSP Process Launch Utilities
3
+ *
4
+ * Handles spawning LSP servers via various methods:
5
+ * - Direct binary execution (using absolute paths on Windows)
6
+ * - Node.js scripts (npx/bun)
7
+ * - Package manager execution
8
+ */
9
+ import { spawn as nodeSpawn, } from "node:child_process";
10
+ import path from "node:path";
11
+ // Helper to detect if running on Windows
12
+ const isWindows = process.platform === "win32";
13
+ /**
14
+ * Attach error handler to a spawned process to prevent ENOENT crashes
15
+ * This catches "command not found" errors and other spawn failures
16
+ */
17
+ function _attachErrorHandler(proc, context) {
18
+ proc.on("error", (err) => {
19
+ // Log the error but don't crash - the caller should handle this gracefully
20
+ console.error(`[lsp] Spawn error for ${context}:`, err.message);
21
+ });
22
+ // Also handle unexpected exit (process crash after successful spawn)
23
+ proc.on("exit", (code, signal) => {
24
+ if (code !== 0 && code !== null) {
25
+ console.error(`[lsp] ${context} exited with code ${code}${signal ? ` (signal: ${signal})` : ""}`);
26
+ }
27
+ });
28
+ proc.on("close", (code, signal) => {
29
+ if (code !== 0 && code !== null) {
30
+ console.error(`[lsp] ${context} closed with code ${code}${signal ? ` (signal: ${signal})` : ""}`);
31
+ }
32
+ });
33
+ }
34
+ /**
35
+ * Spawn an LSP server process
36
+ *
37
+ * Key fixes for Windows:
38
+ * - Uses absolute paths (relative paths fail in shell mode)
39
+ * - Uses shell: true for .cmd files
40
+ * - Uses windowsHide to prevent console window popup
41
+ *
42
+ * @param command - Command to run (e.g., "typescript-language-server")
43
+ * @param args - Arguments (e.g., ["--stdio"])
44
+ * @param options - Spawn options including cwd, env
45
+ * @returns LSPProcess handle
46
+ */
47
+ export function launchLSP(command, args = [], options = {}) {
48
+ const cwd = String(options.cwd ?? process.cwd());
49
+ const env = { ...process.env, ...options.env };
50
+ // Resolve command path
51
+ // - If already absolute, use as-is
52
+ // - If it's a simple command (no path separators), let system find it via PATH
53
+ // - Otherwise, resolve relative to cwd
54
+ const resolvedCommand = path.isAbsolute(command)
55
+ ? command
56
+ : command.includes(path.sep) || command.includes("/")
57
+ ? path.resolve(cwd, command)
58
+ : command; // Let system find it via PATH
59
+ // On Windows with shell: true, we need to quote the command if it has spaces
60
+ const needsShell = isWindows &&
61
+ (resolvedCommand.includes(" ") || resolvedCommand.includes(".cmd"));
62
+ let proc;
63
+ if (needsShell) {
64
+ // Use shell mode with quoted command
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
+ });
74
+ }
75
+ else {
76
+ // Use normal spawn without shell
77
+ proc = nodeSpawn(resolvedCommand, args, {
78
+ cwd,
79
+ env,
80
+ stdio: ["pipe", "pipe", "pipe"],
81
+ detached: false,
82
+ windowsHide: isWindows,
83
+ });
84
+ }
85
+ if (!proc.stdin || !proc.stdout || !proc.stderr) {
86
+ throw new Error(`Failed to spawn LSP server: ${command}`);
87
+ }
88
+ // Check if process exited immediately (spawn failure - synchronous check)
89
+ if (proc.exitCode !== null || proc.killed) {
90
+ throw new Error(`LSP server ${command} exited immediately (code: ${proc.exitCode}). ` +
91
+ `The binary may be missing or corrupted.`);
92
+ }
93
+ // Attach error handler to prevent ENOENT crashes and track later failures
94
+ _attachErrorHandler(proc, command);
95
+ return {
96
+ process: proc,
97
+ stdin: proc.stdin,
98
+ stdout: proc.stdout,
99
+ stderr: proc.stderr,
100
+ pid: proc.pid ?? 0,
101
+ };
102
+ }
103
+ /**
104
+ * Spawn via package manager (npx/bun)
105
+ */
106
+ export function launchViaPackageManager(packageName, args = [], options = {}) {
107
+ // Prefer bun if available, fall back to npx (use .cmd on Windows)
108
+ const isWin = process.platform === "win32";
109
+ if (process.env.BUN_INSTALL) {
110
+ return launchLSP(isWin ? "bun.exe" : "bun", ["x", packageName, ...args], options);
111
+ }
112
+ // For npx on Windows, use shell mode with the full command string
113
+ if (isWin) {
114
+ const argsStr = args.map((a) => (a.includes(" ") ? `"${a}"` : a)).join(" ");
115
+ const shellCommand = `npx -y ${packageName}${argsStr ? ` ${argsStr}` : ""}`;
116
+ const cwd = String(options.cwd ?? process.cwd());
117
+ const env = { ...process.env, ...options.env };
118
+ const proc = nodeSpawn(shellCommand, [], {
119
+ cwd,
120
+ env,
121
+ stdio: ["pipe", "pipe", "pipe"],
122
+ detached: false,
123
+ windowsHide: true,
124
+ shell: true,
125
+ });
126
+ // Attach error handler to prevent ENOENT crashes
127
+ _attachErrorHandler(proc, packageName);
128
+ return {
129
+ process: proc,
130
+ stdin: proc.stdin,
131
+ stdout: proc.stdout,
132
+ stderr: proc.stderr,
133
+ pid: proc.pid ?? 0,
134
+ };
135
+ }
136
+ return launchLSP("npx", ["-y", packageName, ...args], options);
137
+ }
138
+ /**
139
+ * Spawn via Node.js directly
140
+ */
141
+ export function launchViaNode(scriptPath, args = [], options = {}) {
142
+ return launchLSP(process.execPath, [scriptPath, ...args], options);
143
+ }
144
+ /**
145
+ * Spawn via Python module
146
+ */
147
+ export function launchViaPython(moduleName, args = [], options = {}) {
148
+ // On Windows, prefer 'py' launcher, fall back to 'python'
149
+ const pythonCmd = process.platform === "win32" ? "py" : "python3";
150
+ return launchLSP(pythonCmd, ["-m", moduleName, ...args], options);
151
+ }
152
+ /**
153
+ * Stop an LSP process gracefully
154
+ */
155
+ export async function stopLSP(handle) {
156
+ return new Promise((resolve) => {
157
+ // Send SIGTERM first
158
+ handle.process.kill("SIGTERM");
159
+ // Force kill after timeout
160
+ const timeout = setTimeout(() => {
161
+ if (!handle.process.killed) {
162
+ handle.process.kill("SIGKILL");
163
+ }
164
+ }, 5000);
165
+ handle.process.on("exit", () => {
166
+ clearTimeout(timeout);
167
+ resolve();
168
+ });
169
+ handle.process.on("error", () => {
170
+ clearTimeout(timeout);
171
+ resolve();
172
+ });
173
+ });
174
+ }
@@ -0,0 +1,240 @@
1
+ /**
2
+ * LSP Process Launch Utilities
3
+ *
4
+ * Handles spawning LSP servers via various methods:
5
+ * - Direct binary execution (using absolute paths on Windows)
6
+ * - Node.js scripts (npx/bun)
7
+ * - Package manager execution
8
+ */
9
+
10
+ import {
11
+ type ChildProcess,
12
+ spawn as nodeSpawn,
13
+ type SpawnOptions,
14
+ } from "node:child_process";
15
+ import path from "node:path";
16
+
17
+ export interface LSPProcess {
18
+ process: ChildProcess;
19
+ stdin: NodeJS.WritableStream;
20
+ stdout: NodeJS.ReadableStream;
21
+ stderr: NodeJS.ReadableStream;
22
+ pid: number;
23
+ }
24
+
25
+ // Helper to detect if running on Windows
26
+ const isWindows = process.platform === "win32";
27
+
28
+ /**
29
+ * Attach error handler to a spawned process to prevent ENOENT crashes
30
+ * This catches "command not found" errors and other spawn failures
31
+ */
32
+ function _attachErrorHandler(proc: ChildProcess, context: string): void {
33
+ proc.on("error", (err) => {
34
+ // Log the error but don't crash - the caller should handle this gracefully
35
+ console.error(`[lsp] Spawn error for ${context}:`, err.message);
36
+ });
37
+
38
+ // Also handle unexpected exit (process crash after successful spawn)
39
+ proc.on("exit", (code, signal) => {
40
+ if (code !== 0 && code !== null) {
41
+ console.error(
42
+ `[lsp] ${context} exited with code ${code}${signal ? ` (signal: ${signal})` : ""}`,
43
+ );
44
+ }
45
+ });
46
+
47
+ proc.on("close", (code, signal) => {
48
+ if (code !== 0 && code !== null) {
49
+ console.error(
50
+ `[lsp] ${context} closed with code ${code}${signal ? ` (signal: ${signal})` : ""}`,
51
+ );
52
+ }
53
+ });
54
+ }
55
+
56
+ /**
57
+ * Spawn an LSP server process
58
+ *
59
+ * Key fixes for Windows:
60
+ * - Uses absolute paths (relative paths fail in shell mode)
61
+ * - Uses shell: true for .cmd files
62
+ * - Uses windowsHide to prevent console window popup
63
+ *
64
+ * @param command - Command to run (e.g., "typescript-language-server")
65
+ * @param args - Arguments (e.g., ["--stdio"])
66
+ * @param options - Spawn options including cwd, env
67
+ * @returns LSPProcess handle
68
+ */
69
+ export function launchLSP(
70
+ command: string,
71
+ args: string[] = [],
72
+ options: SpawnOptions = {},
73
+ ): LSPProcess {
74
+ const cwd = String(options.cwd ?? process.cwd());
75
+ const env = { ...process.env, ...options.env };
76
+
77
+ // Resolve command path
78
+ // - If already absolute, use as-is
79
+ // - If it's a simple command (no path separators), let system find it via PATH
80
+ // - Otherwise, resolve relative to cwd
81
+ const resolvedCommand = path.isAbsolute(command)
82
+ ? command
83
+ : command.includes(path.sep) || command.includes("/")
84
+ ? path.resolve(cwd, command)
85
+ : command; // Let system find it via PATH
86
+
87
+ // On Windows with shell: true, we need to quote the command if it has spaces
88
+ const needsShell =
89
+ isWindows &&
90
+ (resolvedCommand.includes(" ") || resolvedCommand.includes(".cmd"));
91
+
92
+ let proc: ChildProcess;
93
+
94
+ if (needsShell) {
95
+ // Use shell mode with quoted command
96
+ const shellCommand = `"${resolvedCommand}" ${args.map((a) => (a.includes(" ") ? `"${a}"` : a)).join(" ")}`;
97
+ proc = nodeSpawn(shellCommand, [], {
98
+ cwd,
99
+ env,
100
+ stdio: ["pipe", "pipe", "pipe"],
101
+ detached: false,
102
+ windowsHide: true,
103
+ shell: true,
104
+ });
105
+ } else {
106
+ // Use normal spawn without shell
107
+ proc = nodeSpawn(resolvedCommand, args, {
108
+ cwd,
109
+ env,
110
+ stdio: ["pipe", "pipe", "pipe"],
111
+ detached: false,
112
+ windowsHide: isWindows,
113
+ });
114
+ }
115
+
116
+ if (!proc.stdin || !proc.stdout || !proc.stderr) {
117
+ throw new Error(`Failed to spawn LSP server: ${command}`);
118
+ }
119
+
120
+ // Check if process exited immediately (spawn failure - synchronous check)
121
+ if (proc.exitCode !== null || proc.killed) {
122
+ throw new Error(
123
+ `LSP server ${command} exited immediately (code: ${proc.exitCode}). ` +
124
+ `The binary may be missing or corrupted.`,
125
+ );
126
+ }
127
+
128
+ // Attach error handler to prevent ENOENT crashes and track later failures
129
+ _attachErrorHandler(proc, command);
130
+
131
+ return {
132
+ process: proc,
133
+ stdin: proc.stdin,
134
+ stdout: proc.stdout,
135
+ stderr: proc.stderr,
136
+ pid: proc.pid ?? 0,
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Spawn via package manager (npx/bun)
142
+ */
143
+ export function launchViaPackageManager(
144
+ packageName: string,
145
+ args: string[] = [],
146
+ options: SpawnOptions = {},
147
+ ): LSPProcess {
148
+ // Prefer bun if available, fall back to npx (use .cmd on Windows)
149
+ const isWin = process.platform === "win32";
150
+
151
+ if (process.env.BUN_INSTALL) {
152
+ return launchLSP(
153
+ isWin ? "bun.exe" : "bun",
154
+ ["x", packageName, ...args],
155
+ options,
156
+ );
157
+ }
158
+
159
+ // For npx on Windows, use shell mode with the full command string
160
+ if (isWin) {
161
+ const argsStr = args.map((a) => (a.includes(" ") ? `"${a}"` : a)).join(" ");
162
+ const shellCommand = `npx -y ${packageName}${argsStr ? ` ${argsStr}` : ""}`;
163
+
164
+ const cwd = String(options.cwd ?? process.cwd());
165
+ const env = { ...process.env, ...options.env };
166
+
167
+ const proc = nodeSpawn(shellCommand, [], {
168
+ cwd,
169
+ env,
170
+ stdio: ["pipe", "pipe", "pipe"],
171
+ detached: false,
172
+ windowsHide: true,
173
+ shell: true,
174
+ });
175
+
176
+ // Attach error handler to prevent ENOENT crashes
177
+ _attachErrorHandler(proc, packageName);
178
+
179
+ return {
180
+ process: proc,
181
+ stdin: proc.stdin!,
182
+ stdout: proc.stdout!,
183
+ stderr: proc.stderr!,
184
+ pid: proc.pid ?? 0,
185
+ };
186
+ }
187
+
188
+ return launchLSP("npx", ["-y", packageName, ...args], options);
189
+ }
190
+
191
+ /**
192
+ * Spawn via Node.js directly
193
+ */
194
+ export function launchViaNode(
195
+ scriptPath: string,
196
+ args: string[] = [],
197
+ options: SpawnOptions = {},
198
+ ): LSPProcess {
199
+ return launchLSP(process.execPath, [scriptPath, ...args], options);
200
+ }
201
+
202
+ /**
203
+ * Spawn via Python module
204
+ */
205
+ export function launchViaPython(
206
+ moduleName: string,
207
+ args: string[] = [],
208
+ options: SpawnOptions = {},
209
+ ): LSPProcess {
210
+ // On Windows, prefer 'py' launcher, fall back to 'python'
211
+ const pythonCmd = process.platform === "win32" ? "py" : "python3";
212
+ return launchLSP(pythonCmd, ["-m", moduleName, ...args], options);
213
+ }
214
+
215
+ /**
216
+ * Stop an LSP process gracefully
217
+ */
218
+ export async function stopLSP(handle: LSPProcess): Promise<void> {
219
+ return new Promise((resolve) => {
220
+ // Send SIGTERM first
221
+ handle.process.kill("SIGTERM");
222
+
223
+ // Force kill after timeout
224
+ const timeout = setTimeout(() => {
225
+ if (!handle.process.killed) {
226
+ handle.process.kill("SIGKILL");
227
+ }
228
+ }, 5000);
229
+
230
+ handle.process.on("exit", () => {
231
+ clearTimeout(timeout);
232
+ resolve();
233
+ });
234
+
235
+ handle.process.on("error", () => {
236
+ clearTimeout(timeout);
237
+ resolve();
238
+ });
239
+ });
240
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * LSP Process Launch Utilities
3
+ *
4
+ * Handles spawning LSP servers via various methods:
5
+ * - Direct binary execution (using absolute paths on Windows)
6
+ * - Node.js scripts (npx/bun)
7
+ * - Package manager execution
8
+ */
9
+ import { spawn as nodeSpawn } from "child_process";
10
+ import path from "path";
11
+ // Helper to detect if running on Windows
12
+ const isWindows = process.platform === "win32";
13
+ /**
14
+ * Spawn an LSP server process
15
+ *
16
+ * Key fixes for Windows:
17
+ * - Uses absolute paths (relative paths fail in shell mode)
18
+ * - Uses shell: true for .cmd files
19
+ * - Uses windowsHide to prevent console window popup
20
+ *
21
+ * @param command - Command to run (e.g., "typescript-language-server")
22
+ * @param args - Arguments (e.g., ["--stdio"])
23
+ * @param options - Spawn options including cwd, env
24
+ * @returns LSPProcess handle
25
+ */
26
+ export function launchLSP(command, args = [], options = {}) {
27
+ const cwd = String(options.cwd ?? process.cwd());
28
+ const env = { ...process.env, ...options.env };
29
+ // Always use absolute path (critical for Windows shell mode)
30
+ const resolvedCommand = path.isAbsolute(command)
31
+ ? command
32
+ : path.resolve(cwd, command);
33
+ // On Windows with shell: true, we need to quote the command if it has spaces
34
+ const needsShell = isWindows && (resolvedCommand.includes(" ") || resolvedCommand.includes(".cmd"));
35
+ let proc;
36
+ if (needsShell) {
37
+ // Use shell mode with quoted command
38
+ const shellCommand = `"${resolvedCommand}" ${args.map(a => a.includes(" ") ? `"${a}"` : a).join(" ")}`;
39
+ proc = nodeSpawn(shellCommand, [], {
40
+ cwd,
41
+ env,
42
+ stdio: ["pipe", "pipe", "pipe"],
43
+ detached: false,
44
+ windowsHide: true,
45
+ shell: true,
46
+ });
47
+ }
48
+ else {
49
+ // Use normal spawn without shell
50
+ proc = nodeSpawn(resolvedCommand, args, {
51
+ cwd,
52
+ env,
53
+ stdio: ["pipe", "pipe", "pipe"],
54
+ detached: false,
55
+ windowsHide: isWindows,
56
+ });
57
+ }
58
+ if (!proc.stdin || !proc.stdout || !proc.stderr) {
59
+ throw new Error(`Failed to spawn LSP server: ${command}`);
60
+ }
61
+ return {
62
+ process: proc,
63
+ stdin: proc.stdin,
64
+ stdout: proc.stdout,
65
+ stderr: proc.stderr,
66
+ pid: proc.pid ?? 0,
67
+ };
68
+ }
69
+ /**
70
+ * Spawn via package manager (npx/bun)
71
+ */
72
+ export function launchViaPackageManager(packageName, args = [], options = {}) {
73
+ // Prefer bun if available, fall back to npx (use .cmd on Windows)
74
+ const isWin = process.platform === "win32";
75
+ const manager = process.env.BUN_INSTALL
76
+ ? { cmd: isWin ? "bun.exe" : "bun", args: ["x", packageName, ...args] }
77
+ : { cmd: isWin ? "npx.cmd" : "npx", args: ["-y", packageName, ...args] };
78
+ return launchLSP(manager.cmd, manager.args, options);
79
+ }
80
+ /**
81
+ * Spawn via Node.js directly
82
+ */
83
+ export function launchViaNode(scriptPath, args = [], options = {}) {
84
+ return launchLSP(process.execPath, [scriptPath, ...args], options);
85
+ }
86
+ /**
87
+ * Spawn via Python module
88
+ */
89
+ export function launchViaPython(moduleName, args = [], options = {}) {
90
+ // On Windows, prefer 'py' launcher, fall back to 'python'
91
+ const pythonCmd = process.platform === "win32" ? "py" : "python3";
92
+ return launchLSP(pythonCmd, ["-m", moduleName, ...args], options);
93
+ }
94
+ /**
95
+ * Stop an LSP process gracefully
96
+ */
97
+ export async function stopLSP(handle) {
98
+ return new Promise((resolve) => {
99
+ // Send SIGTERM first
100
+ handle.process.kill("SIGTERM");
101
+ // Force kill after timeout
102
+ const timeout = setTimeout(() => {
103
+ if (!handle.process.killed) {
104
+ handle.process.kill("SIGKILL");
105
+ }
106
+ }, 5000);
107
+ handle.process.on("exit", () => {
108
+ clearTimeout(timeout);
109
+ resolve();
110
+ });
111
+ handle.process.on("error", () => {
112
+ clearTimeout(timeout);
113
+ resolve();
114
+ });
115
+ });
116
+ }