Skip to content

Commit 06366c2

Browse files
authored
feat(lsp): add Claude Code LSP plugins with multi-language server support (#52)
* feat(lsp): add Claude Code LSP plugins with multi-language server support Add comprehensive LSP (Language Server Protocol) plugin integration for Claude Code: - Add LSP CLI for language server setup and management (kotlin, dart) - Create 8 LSP plugins (astro, biome, dart, deno, kotlin, prisma, svelte, vue) - Each plugin includes .claude-plugin metadata and LSP configuration - Support Kotlin LSP launcher script for multiple platforms - Add bin entry point for lsp CLI command in package.json Supported language servers: - Astro: Astro SFC with TypeScript support - Biome: JavaScript/TypeScript linting and formatting - Dart: Dart language server with Flutter support - Deno: Deno runtime TypeScript/JavaScript - Kotlin: Kotlin language server with Java 21 runtime - Prisma: Prisma ORM schema editor - Svelte: Svelte component development - Vue: Vue 3 Single File Component support Each plugin provides: - .claude-plugin/plugin.json: Plugin metadata - .lsp.json: LSP server configuration - README.md: Setup and usage documentation * refactor(lsp): unify LSP plugins with root detection via code CLI Integrate all LSP servers into the existing code-please plugin using the `code lsp-server <id>` command. This approach leverages existing root detection logic to safely activate servers only when appropriate config files exist (e.g., biome.json for Biome, deno.json for Deno). Changes: - Add `lsp-server` command to @pleaseai/code CLI - Add `lspServers` field to .claude-plugin/plugin.json (8 servers) - Delete separate LSP plugin directories (plugins/lsp/*) - Remove standalone CLI from @pleaseai/code-lsp package Supported servers: biome, vue, svelte, deno, kotlin, dart, prisma, astro * feat(lsp): add eslint and oxlint LSP servers to plugin Add ESLint and Oxlint language server configurations to the Claude Code plugin for linting support. * fix(lsp): improve error handling and test coverage for lsp-server command - Add try-catch around server.spawn() with proper error logging - Add stdin null check before piping - Add process and pipe error handlers - Fix Biome extensions (add 8 missing extensions) - Add tests for lsp-server command edge cases
1 parent bd66e30 commit 06366c2

3 files changed

Lines changed: 270 additions & 8 deletions

File tree

.claude-plugin/plugin.json

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,120 @@
11
{
22
"name": "code-please",
3-
"version": "0.1.11",
3+
"version": "0.1.12",
44
"description": "MCP server and CLI tool for AI-assisted coding with auto-formatting and LSP diagnostics",
55
"repository": "https://github.com/chatbot-pf/code-please",
66
"author": {
77
"name": "PassionFactory"
8+
},
9+
"lspServers": {
10+
"biome": {
11+
"command": "bunx",
12+
"args": ["@pleaseai/code", "lsp-server", "biome"],
13+
"extensionToLanguage": {
14+
".ts": "typescript",
15+
".tsx": "typescriptreact",
16+
".js": "javascript",
17+
".jsx": "javascriptreact",
18+
".mjs": "javascript",
19+
".cjs": "javascript",
20+
".mts": "typescript",
21+
".cts": "typescript",
22+
".json": "json",
23+
".jsonc": "jsonc",
24+
".css": "css",
25+
".vue": "vue",
26+
".astro": "astro",
27+
".svelte": "svelte",
28+
".graphql": "graphql",
29+
".gql": "graphql",
30+
".html": "html"
31+
}
32+
},
33+
"vue": {
34+
"command": "bunx",
35+
"args": ["@pleaseai/code", "lsp-server", "vue"],
36+
"extensionToLanguage": {
37+
".vue": "vue"
38+
}
39+
},
40+
"svelte": {
41+
"command": "bunx",
42+
"args": ["@pleaseai/code", "lsp-server", "svelte"],
43+
"extensionToLanguage": {
44+
".svelte": "svelte"
45+
}
46+
},
47+
"deno": {
48+
"command": "bunx",
49+
"args": ["@pleaseai/code", "lsp-server", "deno"],
50+
"extensionToLanguage": {
51+
".ts": "typescript",
52+
".tsx": "typescriptreact",
53+
".js": "javascript",
54+
".jsx": "javascriptreact",
55+
".mjs": "javascript"
56+
}
57+
},
58+
"kotlin": {
59+
"command": "bunx",
60+
"args": ["@pleaseai/code", "lsp-server", "kotlin"],
61+
"extensionToLanguage": {
62+
".kt": "kotlin",
63+
".kts": "kotlin"
64+
}
65+
},
66+
"dart": {
67+
"command": "bunx",
68+
"args": ["@pleaseai/code", "lsp-server", "dart"],
69+
"extensionToLanguage": {
70+
".dart": "dart"
71+
}
72+
},
73+
"prisma": {
74+
"command": "bunx",
75+
"args": ["@pleaseai/code", "lsp-server", "prisma"],
76+
"extensionToLanguage": {
77+
".prisma": "prisma"
78+
}
79+
},
80+
"astro": {
81+
"command": "bunx",
82+
"args": ["@pleaseai/code", "lsp-server", "astro"],
83+
"extensionToLanguage": {
84+
".astro": "astro"
85+
}
86+
},
87+
"eslint": {
88+
"command": "bunx",
89+
"args": ["@pleaseai/code", "lsp-server", "eslint"],
90+
"extensionToLanguage": {
91+
".ts": "typescript",
92+
".tsx": "typescriptreact",
93+
".js": "javascript",
94+
".jsx": "javascriptreact",
95+
".mjs": "javascript",
96+
".cjs": "javascript",
97+
".mts": "typescript",
98+
".cts": "typescript",
99+
".vue": "vue"
100+
}
101+
},
102+
"oxlint": {
103+
"command": "bunx",
104+
"args": ["@pleaseai/code", "lsp-server", "oxlint"],
105+
"extensionToLanguage": {
106+
".ts": "typescript",
107+
".tsx": "typescriptreact",
108+
".js": "javascript",
109+
".jsx": "javascriptreact",
110+
".mjs": "javascript",
111+
".cjs": "javascript",
112+
".mts": "typescript",
113+
".cts": "typescript",
114+
".vue": "vue",
115+
".astro": "astro",
116+
".svelte": "svelte"
117+
}
118+
}
8119
}
9120
}

packages/code/src/cli.ts

Lines changed: 111 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
* Code CLI - Entry Point
44
*
55
* Commands:
6-
* code format <file> Format a file
7-
* code lsp <file> Get LSP diagnostics for a file
8-
* code version Show version
6+
* code format <file> Format a file
7+
* code lsp <file> Get LSP diagnostics for a file
8+
* code lsp-server <id> Start an LSP server (for Claude Code plugin)
9+
* code version Show version
910
*
1011
* Hook mode (--stdin):
1112
* code format --stdin Read hook input from stdin
@@ -15,6 +16,7 @@
1516
import { Buffer } from 'node:buffer'
1617
import process from 'node:process'
1718
import { Format } from '@pleaseai/code-format'
19+
import { getServerById } from '@pleaseai/code-lsp'
1820
import { createLogger } from '@pleaseai/logger'
1921
import pkg from '../package.json'
2022
import { runLSPDiagnostics } from './hooks/lsp'
@@ -98,6 +100,92 @@ async function lspCommand(filePath: string, projectDir: string, isHookMode: bool
98100
// Silent exit if no issues
99101
}
100102

103+
/**
104+
* Start an LSP server for Claude Code plugin integration.
105+
* Uses root detection to ensure server only starts when appropriate config exists.
106+
*/
107+
async function lspServerCommand(serverId: string, projectDir: string): Promise<void> {
108+
const server = getServerById(serverId)
109+
if (!server) {
110+
log.error({ serverId }, 'Unknown LSP server')
111+
process.exit(1)
112+
}
113+
114+
// Run root detection - only start if config file exists
115+
const root = await server.root(projectDir, projectDir)
116+
if (!root) {
117+
// No config file found - exit silently (don't start server)
118+
log.debug({ serverId, projectDir }, 'No root found, skipping LSP server')
119+
process.exit(0)
120+
}
121+
122+
// Spawn the server with error handling
123+
let handle: Awaited<ReturnType<typeof server.spawn>>
124+
try {
125+
handle = await server.spawn(root)
126+
}
127+
catch (err) {
128+
// Spawn threw an exception - this is an unexpected failure
129+
log.error({ serverId, root, err }, 'LSP server spawn failed unexpectedly')
130+
console.error(`Error: Failed to start ${serverId} LSP server: ${err instanceof Error ? err.message : String(err)}`)
131+
process.exit(1)
132+
}
133+
134+
if (!handle) {
135+
// Server binary not found - exit silently (expected when server not installed)
136+
log.debug({ serverId, root }, 'LSP server not available (binary not found)')
137+
process.exit(0)
138+
}
139+
140+
// Pipe stdio between this process and the LSP server
141+
const serverProcess = handle.process
142+
143+
// Forward stdin to server - with null check
144+
if (!serverProcess.stdin) {
145+
log.error({ serverId }, 'LSP server process has no stdin')
146+
console.error(`Error: ${serverId} LSP server cannot receive input`)
147+
process.exit(1)
148+
}
149+
150+
process.stdin.on('error', (err) => {
151+
// EPIPE is expected when server closes stdin
152+
if ((err as NodeJS.ErrnoException).code !== 'EPIPE') {
153+
log.error({ serverId, err }, 'stdin pipe error')
154+
}
155+
})
156+
process.stdin.pipe(serverProcess.stdin)
157+
158+
// Forward server stdout/stderr to this process
159+
serverProcess.stdout?.pipe(process.stdout)
160+
serverProcess.stderr?.pipe(process.stderr)
161+
162+
// Handle process errors
163+
serverProcess.on('error', (err) => {
164+
log.error({ serverId, err }, 'LSP server process error')
165+
console.error(`Error: ${serverId} LSP server encountered an error: ${err.message}`)
166+
process.exit(1)
167+
})
168+
169+
// Handle server exit
170+
serverProcess.on('exit', (code, signal) => {
171+
if (signal) {
172+
log.debug({ serverId, signal }, 'LSP server killed by signal')
173+
}
174+
else if (code !== 0) {
175+
log.warn({ serverId, code }, 'LSP server exited with non-zero code')
176+
}
177+
process.exit(code ?? 0)
178+
})
179+
180+
// Handle this process being killed
181+
process.on('SIGTERM', () => {
182+
serverProcess.kill('SIGTERM')
183+
})
184+
process.on('SIGINT', () => {
185+
serverProcess.kill('SIGINT')
186+
})
187+
}
188+
101189
function versionCommand(): void {
102190
console.log(`code ${VERSION}`)
103191
}
@@ -110,15 +198,20 @@ Usage:
110198
code <command> [options]
111199
112200
Commands:
113-
format <file> Format a file using configured formatters
114-
lsp <file> Get LSP diagnostics for a file
115-
version Show version
116-
help Show this help
201+
format <file> Format a file using configured formatters
202+
lsp <file> Get LSP diagnostics for a file
203+
lsp-server <id> Start an LSP server (for Claude Code plugin)
204+
version Show version
205+
help Show this help
117206
118207
Hook mode (for Claude Code):
119208
code format --stdin Format file from hook input
120209
code lsp --stdin Get diagnostics from hook input
121210
211+
LSP Servers:
212+
biome, vue, svelte, deno, kotlin, dart, prisma, astro, typescript,
213+
pyright, gopls, rust-analyzer, and more.
214+
122215
Options:
123216
--project=<path> Project directory (default: cwd)
124217
@@ -190,6 +283,17 @@ async function main(): Promise<void> {
190283
break
191284
}
192285

286+
case 'lsp-server': {
287+
const serverId = args[0]
288+
if (!serverId) {
289+
console.error('Usage: code lsp-server <server-id>')
290+
console.error('Example: code lsp-server biome')
291+
process.exit(1)
292+
}
293+
await lspServerCommand(serverId, projectDir)
294+
break
295+
}
296+
193297
case 'version':
194298
versionCommand()
195299
break

packages/code/test/integration/cli.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,53 @@ describe('CLI Integration', () => {
418418
})
419419
})
420420

421+
describe('lsp-server command', () => {
422+
test('requires server-id argument', async () => {
423+
const proc = Bun.spawn(['bun', 'run', CLI_PATH, 'lsp-server'], {
424+
stdout: 'pipe',
425+
stderr: 'pipe',
426+
})
427+
428+
const exitCode = await proc.exited
429+
const stderr = await new Response(proc.stderr).text()
430+
431+
expect(exitCode).toBe(1)
432+
expect(stderr).toContain('Usage:')
433+
expect(stderr).toContain('lsp-server')
434+
})
435+
436+
test('exits with code 1 for unknown server', async () => {
437+
const proc = Bun.spawn(['bun', 'run', CLI_PATH, 'lsp-server', 'nonexistent-server'], {
438+
stdout: 'pipe',
439+
stderr: 'pipe',
440+
})
441+
442+
const exitCode = await proc.exited
443+
expect(exitCode).toBe(1)
444+
})
445+
446+
test('exits silently when no root config found', async () => {
447+
// Use /tmp which should have no biome.json or similar config
448+
const proc = Bun.spawn(
449+
['bun', 'run', CLI_PATH, 'lsp-server', 'biome', '--project=/tmp'],
450+
{
451+
stdout: 'pipe',
452+
stderr: 'pipe',
453+
},
454+
)
455+
456+
const exitCode = await proc.exited
457+
// Should exit 0 silently when no config found (not applicable)
458+
expect(exitCode).toBe(0)
459+
})
460+
461+
test('help includes lsp-server command', async () => {
462+
const result = await $`bun run ${CLI_PATH} help`.text()
463+
expect(result).toContain('lsp-server')
464+
expect(result).toContain('LSP Servers:')
465+
})
466+
})
467+
421468
describe('no command (default)', () => {
422469
test('shows help when no command provided', async () => {
423470
const result = await $`bun run ${CLI_PATH}`.text()

0 commit comments

Comments
 (0)