Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 112 additions & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,120 @@
{
"name": "code-please",
"version": "0.1.11",
"version": "0.1.12",
"description": "MCP server and CLI tool for AI-assisted coding with auto-formatting and LSP diagnostics",
"repository": "https://github.com/chatbot-pf/code-please",
"author": {
"name": "PassionFactory"
},
"lspServers": {
"biome": {
"command": "bunx",
"args": ["@pleaseai/code", "lsp-server", "biome"],
"extensionToLanguage": {
".ts": "typescript",
".tsx": "typescriptreact",
".js": "javascript",
".jsx": "javascriptreact",
".mjs": "javascript",
".cjs": "javascript",
".mts": "typescript",
".cts": "typescript",
".json": "json",
".jsonc": "jsonc",
".css": "css",
".vue": "vue",
".astro": "astro",
".svelte": "svelte",
".graphql": "graphql",
".gql": "graphql",
".html": "html"
}
},
"vue": {
"command": "bunx",
"args": ["@pleaseai/code", "lsp-server", "vue"],
"extensionToLanguage": {
".vue": "vue"
}
},
"svelte": {
"command": "bunx",
"args": ["@pleaseai/code", "lsp-server", "svelte"],
"extensionToLanguage": {
".svelte": "svelte"
}
},
"deno": {
"command": "bunx",
"args": ["@pleaseai/code", "lsp-server", "deno"],
"extensionToLanguage": {
".ts": "typescript",
".tsx": "typescriptreact",
".js": "javascript",
".jsx": "javascriptreact",
".mjs": "javascript"
}
},
"kotlin": {
"command": "bunx",
"args": ["@pleaseai/code", "lsp-server", "kotlin"],
"extensionToLanguage": {
".kt": "kotlin",
".kts": "kotlin"
}
},
"dart": {
"command": "bunx",
"args": ["@pleaseai/code", "lsp-server", "dart"],
"extensionToLanguage": {
".dart": "dart"
}
},
"prisma": {
"command": "bunx",
"args": ["@pleaseai/code", "lsp-server", "prisma"],
"extensionToLanguage": {
".prisma": "prisma"
}
},
"astro": {
"command": "bunx",
"args": ["@pleaseai/code", "lsp-server", "astro"],
"extensionToLanguage": {
".astro": "astro"
}
},
"eslint": {
"command": "bunx",
"args": ["@pleaseai/code", "lsp-server", "eslint"],
"extensionToLanguage": {
".ts": "typescript",
".tsx": "typescriptreact",
".js": "javascript",
".jsx": "javascriptreact",
".mjs": "javascript",
".cjs": "javascript",
".mts": "typescript",
".cts": "typescript",
".vue": "vue"
}
},
"oxlint": {
"command": "bunx",
"args": ["@pleaseai/code", "lsp-server", "oxlint"],
"extensionToLanguage": {
".ts": "typescript",
".tsx": "typescriptreact",
".js": "javascript",
".jsx": "javascriptreact",
".mjs": "javascript",
".cjs": "javascript",
".mts": "typescript",
".cts": "typescript",
".vue": "vue",
".astro": "astro",
".svelte": "svelte"
}
}
}
}
118 changes: 111 additions & 7 deletions packages/code/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
* Code CLI - Entry Point
*
* Commands:
* code format <file> Format a file
* code lsp <file> Get LSP diagnostics for a file
* code version Show version
* code format <file> Format a file
* code lsp <file> Get LSP diagnostics for a file
* code lsp-server <id> Start an LSP server (for Claude Code plugin)
* code version Show version
*
* Hook mode (--stdin):
* code format --stdin Read hook input from stdin
Expand All @@ -15,6 +16,7 @@
import { Buffer } from 'node:buffer'
import process from 'node:process'
import { Format } from '@pleaseai/code-format'
import { getServerById } from '@pleaseai/code-lsp'
import { createLogger } from '@pleaseai/logger'
import pkg from '../package.json'
import { runLSPDiagnostics } from './hooks/lsp'
Expand Down Expand Up @@ -98,6 +100,92 @@ async function lspCommand(filePath: string, projectDir: string, isHookMode: bool
// Silent exit if no issues
}

/**
* Start an LSP server for Claude Code plugin integration.
* Uses root detection to ensure server only starts when appropriate config exists.
*/
async function lspServerCommand(serverId: string, projectDir: string): Promise<void> {
const server = getServerById(serverId)
if (!server) {
log.error({ serverId }, 'Unknown LSP server')
process.exit(1)
}

// Run root detection - only start if config file exists
const root = await server.root(projectDir, projectDir)
if (!root) {
// No config file found - exit silently (don't start server)
log.debug({ serverId, projectDir }, 'No root found, skipping LSP server')
process.exit(0)
}

// Spawn the server with error handling
let handle: Awaited<ReturnType<typeof server.spawn>>
try {
handle = await server.spawn(root)
}
catch (err) {
// Spawn threw an exception - this is an unexpected failure
log.error({ serverId, root, err }, 'LSP server spawn failed unexpectedly')
console.error(`Error: Failed to start ${serverId} LSP server: ${err instanceof Error ? err.message : String(err)}`)
process.exit(1)
}

if (!handle) {
// Server binary not found - exit silently (expected when server not installed)
log.debug({ serverId, root }, 'LSP server not available (binary not found)')
process.exit(0)
}

// Pipe stdio between this process and the LSP server
const serverProcess = handle.process

// Forward stdin to server - with null check
if (!serverProcess.stdin) {
log.error({ serverId }, 'LSP server process has no stdin')
console.error(`Error: ${serverId} LSP server cannot receive input`)
process.exit(1)
}

process.stdin.on('error', (err) => {
// EPIPE is expected when server closes stdin
if ((err as NodeJS.ErrnoException).code !== 'EPIPE') {
log.error({ serverId, err }, 'stdin pipe error')
}
})
process.stdin.pipe(serverProcess.stdin)

// Forward server stdout/stderr to this process
serverProcess.stdout?.pipe(process.stdout)
serverProcess.stderr?.pipe(process.stderr)

// Handle process errors
serverProcess.on('error', (err) => {
log.error({ serverId, err }, 'LSP server process error')
console.error(`Error: ${serverId} LSP server encountered an error: ${err.message}`)
process.exit(1)
})

// Handle server exit
serverProcess.on('exit', (code, signal) => {
if (signal) {
log.debug({ serverId, signal }, 'LSP server killed by signal')
}
else if (code !== 0) {
log.warn({ serverId, code }, 'LSP server exited with non-zero code')
}
process.exit(code ?? 0)
})

// Handle this process being killed
process.on('SIGTERM', () => {
serverProcess.kill('SIGTERM')
})
process.on('SIGINT', () => {
serverProcess.kill('SIGINT')
})
}

function versionCommand(): void {
console.log(`code ${VERSION}`)
}
Expand All @@ -110,15 +198,20 @@ Usage:
code <command> [options]

Commands:
format <file> Format a file using configured formatters
lsp <file> Get LSP diagnostics for a file
version Show version
help Show this help
format <file> Format a file using configured formatters
lsp <file> Get LSP diagnostics for a file
lsp-server <id> Start an LSP server (for Claude Code plugin)
version Show version
help Show this help

Hook mode (for Claude Code):
code format --stdin Format file from hook input
code lsp --stdin Get diagnostics from hook input

LSP Servers:
biome, vue, svelte, deno, kotlin, dart, prisma, astro, typescript,
pyright, gopls, rust-analyzer, and more.

Options:
--project=<path> Project directory (default: cwd)

Expand Down Expand Up @@ -190,6 +283,17 @@ async function main(): Promise<void> {
break
}

case 'lsp-server': {
const serverId = args[0]
if (!serverId) {
console.error('Usage: code lsp-server <server-id>')
console.error('Example: code lsp-server biome')
process.exit(1)
}
await lspServerCommand(serverId, projectDir)
break
}

case 'version':
versionCommand()
break
Expand Down
47 changes: 47 additions & 0 deletions packages/code/test/integration/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,53 @@ describe('CLI Integration', () => {
})
})

describe('lsp-server command', () => {
test('requires server-id argument', async () => {
const proc = Bun.spawn(['bun', 'run', CLI_PATH, 'lsp-server'], {
stdout: 'pipe',
stderr: 'pipe',
})

const exitCode = await proc.exited
const stderr = await new Response(proc.stderr).text()

expect(exitCode).toBe(1)
expect(stderr).toContain('Usage:')
expect(stderr).toContain('lsp-server')
})

test('exits with code 1 for unknown server', async () => {
const proc = Bun.spawn(['bun', 'run', CLI_PATH, 'lsp-server', 'nonexistent-server'], {
stdout: 'pipe',
stderr: 'pipe',
})

const exitCode = await proc.exited
expect(exitCode).toBe(1)
})

test('exits silently when no root config found', async () => {
// Use /tmp which should have no biome.json or similar config
const proc = Bun.spawn(
['bun', 'run', CLI_PATH, 'lsp-server', 'biome', '--project=/tmp'],
{
stdout: 'pipe',
stderr: 'pipe',
},
)

const exitCode = await proc.exited
// Should exit 0 silently when no config found (not applicable)
expect(exitCode).toBe(0)
})

test('help includes lsp-server command', async () => {
const result = await $`bun run ${CLI_PATH} help`.text()
expect(result).toContain('lsp-server')
expect(result).toContain('LSP Servers:')
})
})

describe('no command (default)', () => {
test('shows help when no command provided', async () => {
const result = await $`bun run ${CLI_PATH}`.text()
Expand Down