From a060a7c7b9c31dcf3675b8b72d19ac93911211f2 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Thu, 18 Dec 2025 00:30:04 +0900 Subject: [PATCH 1/4] feat(lsp): add Kotlin Language Server support (#6) Add support for JetBrains Kotlin LSP with auto-download capabilities: - Platform detection for win-x64, linux-x64, linux-arm64, osx-x64, osx-arm64 - Auto-download JetBrains Kotlin LSP (v0.253.10629) - Auto-download bundled JRE 21 from vscode-java releases - Root detection via Gradle (build.gradle.kts, build.gradle) and Maven (pom.xml) - Support for .kt and .kts file extensions - Exported KotlinServer from index.ts - Added comprehensive tests for KotlinServer Closes #6 --- .gitmodules | 3 + CLAUDE.md | 1 + packages/lsp/src/__tests__/server.test.ts | 29 +++ packages/lsp/src/index.ts | 1 + packages/lsp/src/server.ts | 289 ++++++++++++++++++++++ ref/multispy | 1 + 6 files changed, 324 insertions(+) create mode 160000 ref/multispy diff --git a/.gitmodules b/.gitmodules index d88d75f..543c06d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "ref/opencode"] path = ref/opencode url = https://github.com/sst/opencode.git +[submodule "ref/multispy"] + path = ref/multispy + url = https://github.com/microsoft/multilspy.git diff --git a/CLAUDE.md b/CLAUDE.md index f8f6681..de86154 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -124,6 +124,7 @@ Claude/MCP Client <-> StdioTransport <-> McpServer <-> Providers | Python | pyright-langserver | pyproject.toml, setup.py, requirements.txt | | Go | gopls | go.mod, go.work | | Rust | rust-analyzer | Cargo.toml | +| Kotlin | JetBrains Kotlin LSP (auto-download) | build.gradle.kts, build.gradle, pom.xml | ### Built-in Formatters diff --git a/packages/lsp/src/__tests__/server.test.ts b/packages/lsp/src/__tests__/server.test.ts index b2e9254..483e143 100644 --- a/packages/lsp/src/__tests__/server.test.ts +++ b/packages/lsp/src/__tests__/server.test.ts @@ -4,6 +4,7 @@ import { getServerById, getServersForExtension, GoplsServer, + KotlinServer, LSP_SERVERS, OxlintServer, PyrightServer, @@ -22,6 +23,7 @@ describe('LSP_SERVERS', () => { expect(serverIds).toContain('pyright') expect(serverIds).toContain('gopls') expect(serverIds).toContain('rust-analyzer') + expect(serverIds).toContain('kotlin') }) }) @@ -120,6 +122,25 @@ describe('RustAnalyzerServer', () => { }) }) +describe('KotlinServer', () => { + test('has correct id', () => { + expect(KotlinServer.id).toBe('kotlin') + }) + + test('supports Kotlin extensions', () => { + expect(KotlinServer.extensions).toContain('.kt') + expect(KotlinServer.extensions).toContain('.kts') + }) + + test('has root function', () => { + expect(typeof KotlinServer.root).toBe('function') + }) + + test('has spawn function', () => { + expect(typeof KotlinServer.spawn).toBe('function') + }) +}) + describe('getServerById', () => { test('returns typescript server', () => { const server = getServerById('typescript') @@ -158,6 +179,14 @@ describe('getServersForExtension', () => { expect(serverIds).toContain('gopls') }) + test('returns servers for .kt extension', () => { + const servers = getServersForExtension('.kt') + expect(servers.length).toBeGreaterThan(0) + + const serverIds = servers.map(s => s.id) + expect(serverIds).toContain('kotlin') + }) + test('returns empty array for unknown extension', () => { const servers = getServersForExtension('.unknown') expect(servers).toEqual([]) diff --git a/packages/lsp/src/index.ts b/packages/lsp/src/index.ts index 93465c9..a72c471 100644 --- a/packages/lsp/src/index.ts +++ b/packages/lsp/src/index.ts @@ -396,6 +396,7 @@ export { getServerById, getServersForExtension, GoplsServer, + KotlinServer, LSP_SERVERS, type LSPServerHandle, type LSPServerInfo, diff --git a/packages/lsp/src/server.ts b/packages/lsp/src/server.ts index 25a6d26..b22f7a1 100644 --- a/packages/lsp/src/server.ts +++ b/packages/lsp/src/server.ts @@ -5,9 +5,12 @@ import type { ChildProcessWithoutNullStreams } from 'node:child_process' import { spawn } from 'node:child_process' +import { createWriteStream } from 'node:fs' import fs from 'node:fs/promises' +import os from 'node:os' import path from 'node:path' import process from 'node:process' +import { pipeline } from 'node:stream/promises' export interface LSPServerHandle { process: ChildProcessWithoutNullStreams @@ -26,6 +29,109 @@ export interface LSPServerInfo { spawn: (root: string) => Promise } +// ============================================================================= +// Platform Detection Utilities +// ============================================================================= + +/** + * Supported platform identifiers for auto-download dependencies + * Note: win-arm64 is not supported as JRE distributions are not available + */ +type PlatformId = 'win-x64' | 'linux-x64' | 'linux-arm64' | 'osx-x64' | 'osx-arm64' + +/** + * Get platform identifier for current system + */ +function getPlatformId(): PlatformId | undefined { + const platform = process.platform + const arch = process.arch + + const platformMap: Record = { + win32: 'win', + darwin: 'osx', + linux: 'linux', + } + + const archMap: Record = { + x64: 'x64', + arm64: 'arm64', + } + + const platformStr = platformMap[platform] + const archStr = archMap[arch] + + if (!platformStr || !archStr) + return undefined + + return `${platformStr}-${archStr}` as PlatformId +} + +// ============================================================================= +// Download Utilities +// ============================================================================= + +/** + * Download a file from URL to destination + */ +async function downloadFile(url: string, dest: string): Promise { + const response = await fetch(url, { redirect: 'follow' }) + if (!response.ok) { + throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`) + } + + const body = response.body + if (!body) { + throw new Error(`No response body for ${url}`) + } + + await fs.mkdir(path.dirname(dest), { recursive: true }) + const writeStream = createWriteStream(dest) + await pipeline(body as unknown as NodeJS.ReadableStream, writeStream) +} + +/** + * Extract a zip archive using Bun's native support + */ +async function extractZip(zipPath: string, destDir: string): Promise { + await fs.mkdir(destDir, { recursive: true }) + + // Use Bun's built-in unzip capability via shell + const proc = Bun.spawn(['unzip', '-o', '-q', zipPath, '-d', destDir], { + stdout: 'pipe', + stderr: 'pipe', + }) + const exitCode = await proc.exited + + if (exitCode !== 0) { + const stderr = await new Response(proc.stderr).text() + throw new Error(`Failed to extract ${zipPath}: ${stderr}`) + } +} + +/** + * Download and extract an archive + */ +async function downloadAndExtract(url: string, destDir: string): Promise { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'lsp-download-')) + const tempFile = path.join(tempDir, 'archive.zip') + + try { + console.warn(`[lsp] Downloading ${url}...`) + await downloadFile(url, tempFile) + console.warn(`[lsp] Extracting to ${destDir}...`) + await extractZip(tempFile, destDir) + console.warn(`[lsp] Download complete`) + } + finally { + // Cleanup temp files + await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {}) + } +} + +// ============================================================================= +// Root Detection Utilities +// ============================================================================= + /** * Find nearest directory containing one of the target files */ @@ -329,6 +435,188 @@ export const OxlintServer: LSPServerInfo = { }, } +// ============================================================================= +// Kotlin Language Server +// ============================================================================= + +/** + * Kotlin Language Server runtime dependency configuration + * Uses official JetBrains Kotlin LSP and bundled JRE 21 + */ +const KOTLIN_RUNTIME_DEPS = { + kotlinLsp: { + url: 'https://download-cdn.jetbrains.com/kotlin-lsp/0.253.10629/kotlin-0.253.10629.zip', + version: '0.253.10629', + }, + java: { + 'win-x64': { + url: 'https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-win32-x64-1.42.0-561.vsix', + javaHomePath: 'extension/jre/21.0.7-win32-x86_64', + javaPath: 'extension/jre/21.0.7-win32-x86_64/bin/java.exe', + }, + 'linux-x64': { + url: 'https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-x64-1.42.0-561.vsix', + javaHomePath: 'extension/jre/21.0.7-linux-x86_64', + javaPath: 'extension/jre/21.0.7-linux-x86_64/bin/java', + }, + 'linux-arm64': { + url: 'https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-arm64-1.42.0-561.vsix', + javaHomePath: 'extension/jre/21.0.7-linux-aarch64', + javaPath: 'extension/jre/21.0.7-linux-aarch64/bin/java', + }, + 'osx-x64': { + url: 'https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-x64-1.42.0-561.vsix', + javaHomePath: 'extension/jre/21.0.7-macosx-x86_64', + javaPath: 'extension/jre/21.0.7-macosx-x86_64/bin/java', + }, + 'osx-arm64': { + url: 'https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix', + javaHomePath: 'extension/jre/21.0.7-macosx-aarch64', + javaPath: 'extension/jre/21.0.7-macosx-aarch64/bin/java', + }, + } as Record, +} + +/** + * Get the Kotlin LSP resources directory + */ +function getKotlinResourcesDir(): string { + // Store in user's home directory under .cache/dora/kotlin-lsp + return path.join(os.homedir(), '.cache', 'dora', 'kotlin-lsp') +} + +/** + * Setup Kotlin runtime dependencies (Java + Kotlin LSP) + * Downloads and extracts if not already present + */ +async function setupKotlinDependencies(platformId: PlatformId): Promise<{ + javaHomePath: string + kotlinLspPath: string +} | undefined> { + const resourcesDir = getKotlinResourcesDir() + const javaConfig = KOTLIN_RUNTIME_DEPS.java[platformId] + + if (!javaConfig) { + console.warn(`[kotlin] Unsupported platform: ${platformId}`) + return undefined + } + + // Setup Java + const javaDir = path.join(resourcesDir, 'java') + const javaHomePath = path.join(javaDir, javaConfig.javaHomePath) + const javaPath = path.join(javaDir, javaConfig.javaPath) + + try { + await fs.access(javaPath) + } + catch { + // Java not found, download it + console.warn(`[kotlin] Downloading Java 21 for ${platformId}...`) + try { + await downloadAndExtract(javaConfig.url, javaDir) + + // Make Java executable on Unix platforms + if (!platformId.startsWith('win-')) { + await fs.chmod(javaPath, 0o755).catch(() => {}) + } + } + catch (err) { + console.error(`[kotlin] Failed to download Java:`, err) + return undefined + } + } + + // Verify Java exists + try { + await fs.access(javaPath) + } + catch { + console.error(`[kotlin] Java executable not found at ${javaPath}`) + return undefined + } + + // Setup Kotlin LSP + const isWindows = platformId.startsWith('win-') + const kotlinLspScript = isWindows ? 'kotlin-lsp.cmd' : 'kotlin-lsp.sh' + const kotlinLspPath = path.join(resourcesDir, kotlinLspScript) + + try { + await fs.access(kotlinLspPath) + } + catch { + // Kotlin LSP not found, download it + console.warn(`[kotlin] Downloading Kotlin Language Server...`) + try { + await downloadAndExtract(KOTLIN_RUNTIME_DEPS.kotlinLsp.url, resourcesDir) + + // Make script executable on Unix platforms + if (!isWindows) { + await fs.chmod(kotlinLspPath, 0o755).catch(() => {}) + } + } + catch (err) { + console.error(`[kotlin] Failed to download Kotlin LSP:`, err) + return undefined + } + } + + // Verify Kotlin LSP exists + try { + await fs.access(kotlinLspPath) + } + catch { + console.error(`[kotlin] Kotlin LSP script not found at ${kotlinLspPath}`) + return undefined + } + + return { + javaHomePath, + kotlinLspPath, + } +} + +/** + * Kotlin Language Server + * Uses official JetBrains Kotlin LSP with auto-download + */ +export const KotlinServer: LSPServerInfo = { + id: 'kotlin', + extensions: ['.kt', '.kts'], + root: nearestRoot([ + 'build.gradle.kts', + 'build.gradle', + 'settings.gradle.kts', + 'settings.gradle', + 'pom.xml', + ]), + async spawn(root) { + const platformId = getPlatformId() + if (!platformId) { + console.warn(`[kotlin] Unsupported platform: ${process.platform}-${process.arch}`) + return undefined + } + + // Setup dependencies (downloads if needed) + const deps = await setupKotlinDependencies(platformId) + if (!deps) { + return undefined + } + + const { javaHomePath, kotlinLspPath } = deps + + // Spawn Kotlin LSP with JAVA_HOME + const proc = spawn(kotlinLspPath, ['--stdio'], { + cwd: root, + env: { + ...process.env, + JAVA_HOME: javaHomePath, + }, + }) + + return { process: proc } + }, +} + /** * All available LSP servers */ @@ -339,6 +627,7 @@ export const LSP_SERVERS: LSPServerInfo[] = [ PyrightServer, GoplsServer, RustAnalyzerServer, + KotlinServer, ] /** diff --git a/ref/multispy b/ref/multispy new file mode 160000 index 0000000..ee0f6dc --- /dev/null +++ b/ref/multispy @@ -0,0 +1 @@ +Subproject commit ee0f6dcd1c0f4c5471d8078580343203b0c73fa7 From a0f40d6793d8475ca15b5c4ccecfe34d2531a879 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Thu, 18 Dec 2025 00:31:33 +0900 Subject: [PATCH 2/4] docs: update README and add lsp package CLAUDE.md - Add Kotlin LSP to README supported languages table - Create CLAUDE.md for packages/lsp with architecture overview --- README.md | 1 + packages/lsp/CLAUDE.md | 112 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 packages/lsp/CLAUDE.md diff --git a/README.md b/README.md index 95b2378..4aa5398 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ Create `dora.json` or `opencode.json` in your project root: | Python | pyright | pyproject.toml, requirements.txt | | Go | gopls | go.mod | | Rust | rust-analyzer | Cargo.toml | +| Kotlin | JetBrains Kotlin LSP | build.gradle.kts, pom.xml | ### Formatters diff --git a/packages/lsp/CLAUDE.md b/packages/lsp/CLAUDE.md new file mode 100644 index 0000000..2dc9ae8 --- /dev/null +++ b/packages/lsp/CLAUDE.md @@ -0,0 +1,112 @@ +# @pleaseai/code-lsp + +LSP (Language Server Protocol) client implementation for AI coding tools. + +## Overview + +This package provides a unified interface for interacting with multiple language servers, enabling real-time diagnostics, hover information, and symbol navigation. + +## Architecture + +``` +src/ +├── index.ts # Public API, LSPManager class +├── client.ts # LSP client implementation (JSON-RPC) +├── server.ts # LSP server definitions +└── language.ts # Language ID mapping +``` + +## Supported Language Servers + +| Server | ID | Extensions | Root Detection | +|--------|-----|------------|----------------| +| TypeScript | `typescript` | .ts, .tsx, .js, .jsx | package-lock.json, bun.lock, yarn.lock, pnpm-lock.yaml | +| Deno | `deno` | .ts, .tsx, .js | deno.json, deno.jsonc | +| Oxlint | `oxlint` | .ts, .tsx, .js, .jsx, .vue, .astro, .svelte | .oxlintrc.json, package.json | +| Pyright | `pyright` | .py, .pyi | pyproject.toml, setup.py, requirements.txt | +| Gopls | `gopls` | .go | go.mod, go.work | +| Rust Analyzer | `rust-analyzer` | .rs | Cargo.toml | +| Kotlin | `kotlin` | .kt, .kts | build.gradle.kts, build.gradle, pom.xml | + +## Adding a New Server + +1. Define the server in `server.ts`: +```typescript +export const MyServer: LSPServerInfo = { + id: 'my-server', + extensions: ['.ext'], + root: nearestRoot(['config.json']), // or custom root function + async spawn(root) { + const proc = spawn('my-lsp', ['--stdio'], { cwd: root }) + return { process: proc } + }, +} +``` + +2. Add to `LSP_SERVERS` array in `server.ts` + +3. Export from `index.ts` + +4. Add tests in `__tests__/server.test.ts` + +## Auto-Download Pattern (Kotlin Example) + +For servers requiring runtime dependencies: + +```typescript +const RUNTIME_DEPS = { + lsp: { url: '...', version: '...' }, + runtime: { 'platform-id': { url: '...', path: '...' } }, +} + +async function setupDependencies(platform: PlatformId) { + const cacheDir = path.join(os.homedir(), '.cache', 'my-lsp') + // Download and extract if not exists + return { lspPath, runtimePath } +} +``` + +## Key APIs + +### LSPManager + +Main entry point for managing LSP clients: + +```typescript +const manager = new LSPManager(projectPath) + +// Touch file to initialize LSP +await manager.touchFile('src/index.ts', true) + +// Get diagnostics +const diags = await manager.diagnostics() + +// Get hover info +const hover = await manager.hover({ file, line, character }) + +// Search symbols +const symbols = await manager.workspaceSymbol('query') + +// Cleanup +await manager.shutdown() +``` + +### Server Utilities + +```typescript +import { getServerById, getServersForExtension } from '@pleaseai/code-lsp' + +const server = getServerById('typescript') +const servers = getServersForExtension('.ts') +``` + +## Testing + +```bash +bun test ./src +``` + +Tests cover: +- Server definitions (ID, extensions, root, spawn functions) +- LSP client lifecycle +- Manager operations From ce759f61396b691f638f1c7b71a08b479a092f21 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Thu, 18 Dec 2025 00:42:48 +0900 Subject: [PATCH 3/4] fix(lsp): improve error handling and documentation accuracy Error handling improvements: - Replace empty .catch(() => {}) with proper error logging - Add specific ENOENT checks instead of broad catch blocks - Add try-catch around spawn() with error/exit event listeners - Add user guidance message for dependency setup failures - Add error context to verification failure messages Documentation fixes: - Fix misleading extractZip comment (system unzip, not Bun native) - Update CLAUDE.md with accurate extension lists for all servers - Add missing root detection files (go.sum, pyrightconfig.json, etc.) - Update auto-download pattern example to match implementation Test improvements: - Add .kts extension test for getServersForExtension --- packages/lsp/CLAUDE.md | 33 ++++---- packages/lsp/src/__tests__/server.test.ts | 8 ++ packages/lsp/src/server.ts | 95 ++++++++++++++++++----- 3 files changed, 103 insertions(+), 33 deletions(-) diff --git a/packages/lsp/CLAUDE.md b/packages/lsp/CLAUDE.md index 2dc9ae8..a145996 100644 --- a/packages/lsp/CLAUDE.md +++ b/packages/lsp/CLAUDE.md @@ -20,13 +20,13 @@ src/ | Server | ID | Extensions | Root Detection | |--------|-----|------------|----------------| -| TypeScript | `typescript` | .ts, .tsx, .js, .jsx | package-lock.json, bun.lock, yarn.lock, pnpm-lock.yaml | -| Deno | `deno` | .ts, .tsx, .js | deno.json, deno.jsonc | -| Oxlint | `oxlint` | .ts, .tsx, .js, .jsx, .vue, .astro, .svelte | .oxlintrc.json, package.json | -| Pyright | `pyright` | .py, .pyi | pyproject.toml, setup.py, requirements.txt | -| Gopls | `gopls` | .go | go.mod, go.work | -| Rust Analyzer | `rust-analyzer` | .rs | Cargo.toml | -| Kotlin | `kotlin` | .kt, .kts | build.gradle.kts, build.gradle, pom.xml | +| TypeScript | `typescript` | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts | package-lock.json, bun.lockb, bun.lock, yarn.lock, pnpm-lock.yaml | +| Deno | `deno` | .ts, .tsx, .js, .jsx, .mjs | deno.json, deno.jsonc | +| Oxlint | `oxlint` | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts, .vue, .astro, .svelte | .oxlintrc.json, package-lock.json, bun.lockb, bun.lock, pnpm-lock.yaml, yarn.lock, package.json | +| Pyright | `pyright` | .py, .pyi | pyproject.toml, setup.py, requirements.txt, pyrightconfig.json | +| Gopls | `gopls` | .go | go.work, go.mod, go.sum | +| Rust Analyzer | `rust-analyzer` | .rs | Cargo.toml, Cargo.lock | +| Kotlin | `kotlin` | .kt, .kts | build.gradle.kts, build.gradle, settings.gradle.kts, settings.gradle, pom.xml | ## Adding a New Server @@ -54,15 +54,20 @@ export const MyServer: LSPServerInfo = { For servers requiring runtime dependencies: ```typescript -const RUNTIME_DEPS = { - lsp: { url: '...', version: '...' }, - runtime: { 'platform-id': { url: '...', path: '...' } }, +const KOTLIN_RUNTIME_DEPS = { + kotlinLsp: { url: '...', version: '...' }, + java: { + 'win-x64': { url: '...', javaHomePath: '...', javaPath: '...' }, + 'linux-x64': { url: '...', javaHomePath: '...', javaPath: '...' }, + // ... other platforms + } as Record, } -async function setupDependencies(platform: PlatformId) { - const cacheDir = path.join(os.homedir(), '.cache', 'my-lsp') - // Download and extract if not exists - return { lspPath, runtimePath } +async function setupKotlinDependencies(platformId: PlatformId) { + const cacheDir = path.join(os.homedir(), '.cache', 'dora', 'kotlin-lsp') + // Check if exists, download and extract if not + // Verify files exist after download + return { javaHomePath, kotlinLspPath } } ``` diff --git a/packages/lsp/src/__tests__/server.test.ts b/packages/lsp/src/__tests__/server.test.ts index 483e143..81d6d2e 100644 --- a/packages/lsp/src/__tests__/server.test.ts +++ b/packages/lsp/src/__tests__/server.test.ts @@ -187,6 +187,14 @@ describe('getServersForExtension', () => { expect(serverIds).toContain('kotlin') }) + test('returns servers for .kts extension', () => { + const servers = getServersForExtension('.kts') + expect(servers.length).toBeGreaterThan(0) + + const serverIds = servers.map(s => s.id) + expect(serverIds).toContain('kotlin') + }) + test('returns empty array for unknown extension', () => { const servers = getServersForExtension('.unknown') expect(servers).toEqual([]) diff --git a/packages/lsp/src/server.ts b/packages/lsp/src/server.ts index b22f7a1..1f89ce4 100644 --- a/packages/lsp/src/server.ts +++ b/packages/lsp/src/server.ts @@ -90,12 +90,12 @@ async function downloadFile(url: string, dest: string): Promise { } /** - * Extract a zip archive using Bun's native support + * Extract a zip archive using system unzip command */ async function extractZip(zipPath: string, destDir: string): Promise { await fs.mkdir(destDir, { recursive: true }) - // Use Bun's built-in unzip capability via shell + // Use system unzip command via Bun.spawn const proc = Bun.spawn(['unzip', '-o', '-q', zipPath, '-d', destDir], { stdout: 'pipe', stderr: 'pipe', @@ -124,7 +124,9 @@ async function downloadAndExtract(url: string, destDir: string): Promise { } finally { // Cleanup temp files - await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {}) + await fs.rm(tempDir, { recursive: true, force: true }).catch((err) => { + console.warn(`[lsp] Failed to cleanup temp directory ${tempDir}:`, err instanceof Error ? err.message : err) + }) } } @@ -509,7 +511,17 @@ async function setupKotlinDependencies(platformId: PlatformId): Promise<{ try { await fs.access(javaPath) } - catch { + catch (err) { + // Check if it's actually a "not found" error vs other access issues + const isNotFound = err instanceof Error + && 'code' in err + && (err as NodeJS.ErrnoException).code === 'ENOENT' + + if (!isNotFound) { + console.error(`[kotlin] Cannot access Java at ${javaPath}:`, err instanceof Error ? err.message : err) + return undefined + } + // Java not found, download it console.warn(`[kotlin] Downloading Java 21 for ${platformId}...`) try { @@ -517,7 +529,13 @@ async function setupKotlinDependencies(platformId: PlatformId): Promise<{ // Make Java executable on Unix platforms if (!platformId.startsWith('win-')) { - await fs.chmod(javaPath, 0o755).catch(() => {}) + try { + await fs.chmod(javaPath, 0o755) + } + catch (chmodErr) { + console.error(`[kotlin] Failed to make Java executable at ${javaPath}:`, chmodErr instanceof Error ? chmodErr.message : chmodErr) + return undefined + } } } catch (err) { @@ -530,8 +548,9 @@ async function setupKotlinDependencies(platformId: PlatformId): Promise<{ try { await fs.access(javaPath) } - catch { - console.error(`[kotlin] Java executable not found at ${javaPath}`) + catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err) + console.error(`[kotlin] Java executable not accessible at ${javaPath}: ${errorMsg}`) return undefined } @@ -543,7 +562,17 @@ async function setupKotlinDependencies(platformId: PlatformId): Promise<{ try { await fs.access(kotlinLspPath) } - catch { + catch (err) { + // Check if it's actually a "not found" error vs other access issues + const isNotFound = err instanceof Error + && 'code' in err + && (err as NodeJS.ErrnoException).code === 'ENOENT' + + if (!isNotFound) { + console.error(`[kotlin] Cannot access Kotlin LSP at ${kotlinLspPath}:`, err instanceof Error ? err.message : err) + return undefined + } + // Kotlin LSP not found, download it console.warn(`[kotlin] Downloading Kotlin Language Server...`) try { @@ -551,7 +580,13 @@ async function setupKotlinDependencies(platformId: PlatformId): Promise<{ // Make script executable on Unix platforms if (!isWindows) { - await fs.chmod(kotlinLspPath, 0o755).catch(() => {}) + try { + await fs.chmod(kotlinLspPath, 0o755) + } + catch (chmodErr) { + console.error(`[kotlin] Failed to make Kotlin LSP executable at ${kotlinLspPath}:`, chmodErr instanceof Error ? chmodErr.message : chmodErr) + return undefined + } } } catch (err) { @@ -564,8 +599,9 @@ async function setupKotlinDependencies(platformId: PlatformId): Promise<{ try { await fs.access(kotlinLspPath) } - catch { - console.error(`[kotlin] Kotlin LSP script not found at ${kotlinLspPath}`) + catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err) + console.error(`[kotlin] Kotlin LSP script not accessible at ${kotlinLspPath}: ${errorMsg}`) return undefined } @@ -599,21 +635,42 @@ export const KotlinServer: LSPServerInfo = { // Setup dependencies (downloads if needed) const deps = await setupKotlinDependencies(platformId) if (!deps) { + console.warn(`[kotlin] Failed to setup Kotlin LSP dependencies. Check previous logs for details.`) return undefined } const { javaHomePath, kotlinLspPath } = deps // Spawn Kotlin LSP with JAVA_HOME - const proc = spawn(kotlinLspPath, ['--stdio'], { - cwd: root, - env: { - ...process.env, - JAVA_HOME: javaHomePath, - }, - }) + try { + const proc = spawn(kotlinLspPath, ['--stdio'], { + cwd: root, + env: { + ...process.env, + JAVA_HOME: javaHomePath, + }, + }) - return { process: proc } + // Log spawn errors + proc.on('error', (err) => { + console.error(`[kotlin] Kotlin LSP process error:`, err) + }) + + proc.on('exit', (code, signal) => { + if (code !== 0 && code !== null) { + console.error(`[kotlin] Kotlin LSP exited with code ${code}`) + } + if (signal) { + console.error(`[kotlin] Kotlin LSP killed by signal ${signal}`) + } + }) + + return { process: proc } + } + catch (err) { + console.error(`[kotlin] Failed to spawn Kotlin LSP:`, err) + return undefined + } }, } From 2c5d21ce0ff6ba3113cde8683bd13dc9722d5211 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Thu, 18 Dec 2025 00:44:22 +0900 Subject: [PATCH 4/4] style: fix whitespace in CLAUDE.md --- packages/lsp/CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lsp/CLAUDE.md b/packages/lsp/CLAUDE.md index a145996..5a46dd0 100644 --- a/packages/lsp/CLAUDE.md +++ b/packages/lsp/CLAUDE.md @@ -35,7 +35,7 @@ src/ export const MyServer: LSPServerInfo = { id: 'my-server', extensions: ['.ext'], - root: nearestRoot(['config.json']), // or custom root function + root: nearestRoot(['config.json']), // or custom root function async spawn(root) { const proc = spawn('my-lsp', ['--stdio'], { cwd: root }) return { process: proc }