init claude-code
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
import { logForDebugging } from '../../utils/debug.js'
|
||||
import { isBareMode } from '../../utils/envUtils.js'
|
||||
import { errorMessage } from '../../utils/errors.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import {
|
||||
createLSPServerManager,
|
||||
type LSPServerManager,
|
||||
} from './LSPServerManager.js'
|
||||
import { registerLSPNotificationHandlers } from './passiveFeedback.js'
|
||||
|
||||
/**
|
||||
* Initialization state of the LSP server manager
|
||||
*/
|
||||
type InitializationState = 'not-started' | 'pending' | 'success' | 'failed'
|
||||
|
||||
/**
|
||||
* Global singleton instance of the LSP server manager.
|
||||
* Initialized during Claude Code startup.
|
||||
*/
|
||||
let lspManagerInstance: LSPServerManager | undefined
|
||||
|
||||
/**
|
||||
* Current initialization state
|
||||
*/
|
||||
let initializationState: InitializationState = 'not-started'
|
||||
|
||||
/**
|
||||
* Error from last initialization attempt, if any
|
||||
*/
|
||||
let initializationError: Error | undefined
|
||||
|
||||
/**
|
||||
* Generation counter to prevent stale initialization promises from updating state
|
||||
*/
|
||||
let initializationGeneration = 0
|
||||
|
||||
/**
|
||||
* Promise that resolves when initialization completes (success or failure)
|
||||
*/
|
||||
let initializationPromise: Promise<void> | undefined
|
||||
|
||||
/**
|
||||
* Test-only sync reset. shutdownLspServerManager() is async and tears down
|
||||
* real connections; this only clears the module-scope singleton state so
|
||||
* reinitializeLspServerManager() early-returns on 'not-started' in downstream
|
||||
* tests on the same shard.
|
||||
*/
|
||||
export function _resetLspManagerForTesting(): void {
|
||||
initializationState = 'not-started'
|
||||
initializationError = undefined
|
||||
initializationPromise = undefined
|
||||
initializationGeneration++
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the singleton LSP server manager instance.
|
||||
* Returns undefined if not yet initialized, initialization failed, or still pending.
|
||||
*
|
||||
* Callers should check for undefined and handle gracefully, as initialization happens
|
||||
* asynchronously during Claude Code startup. Use getInitializationStatus() to
|
||||
* distinguish between pending, failed, and not-started states.
|
||||
*/
|
||||
export function getLspServerManager(): LSPServerManager | undefined {
|
||||
// Don't return a broken instance if initialization failed
|
||||
if (initializationState === 'failed') {
|
||||
return undefined
|
||||
}
|
||||
return lspManagerInstance
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current initialization status of the LSP server manager.
|
||||
*
|
||||
* @returns Status object with current state and error (if failed)
|
||||
*/
|
||||
export function getInitializationStatus():
|
||||
| { status: 'not-started' }
|
||||
| { status: 'pending' }
|
||||
| { status: 'success' }
|
||||
| { status: 'failed'; error: Error } {
|
||||
if (initializationState === 'failed') {
|
||||
return {
|
||||
status: 'failed',
|
||||
error: initializationError || new Error('Initialization failed'),
|
||||
}
|
||||
}
|
||||
if (initializationState === 'not-started') {
|
||||
return { status: 'not-started' }
|
||||
}
|
||||
if (initializationState === 'pending') {
|
||||
return { status: 'pending' }
|
||||
}
|
||||
return { status: 'success' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether at least one language server is connected and healthy.
|
||||
* Backs LSPTool.isEnabled().
|
||||
*/
|
||||
export function isLspConnected(): boolean {
|
||||
if (initializationState === 'failed') return false
|
||||
const manager = getLspServerManager()
|
||||
if (!manager) return false
|
||||
const servers = manager.getAllServers()
|
||||
if (servers.size === 0) return false
|
||||
for (const server of servers.values()) {
|
||||
if (server.state !== 'error') return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for LSP server manager initialization to complete.
|
||||
*
|
||||
* Returns immediately if initialization has already completed (success or failure).
|
||||
* If initialization is pending, waits for it to complete.
|
||||
* If initialization hasn't started, returns immediately.
|
||||
*
|
||||
* @returns Promise that resolves when initialization is complete
|
||||
*/
|
||||
export async function waitForInitialization(): Promise<void> {
|
||||
// If already initialized or failed, return immediately
|
||||
if (initializationState === 'success' || initializationState === 'failed') {
|
||||
return
|
||||
}
|
||||
|
||||
// If pending and we have a promise, wait for it
|
||||
if (initializationState === 'pending' && initializationPromise) {
|
||||
await initializationPromise
|
||||
}
|
||||
|
||||
// If not started, return immediately (nothing to wait for)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the LSP server manager singleton.
|
||||
*
|
||||
* This function is called during Claude Code startup. It synchronously creates
|
||||
* the manager instance, then starts async initialization (loading LSP configs)
|
||||
* in the background without blocking the startup process.
|
||||
*
|
||||
* Safe to call multiple times - will only initialize once (idempotent).
|
||||
* However, if initialization previously failed, calling again will retry.
|
||||
*/
|
||||
export function initializeLspServerManager(): void {
|
||||
// --bare / SIMPLE: no LSP. LSP is for editor integration (diagnostics,
|
||||
// hover, go-to-def in the REPL). Scripted -p calls have no use for it.
|
||||
if (isBareMode()) {
|
||||
return
|
||||
}
|
||||
logForDebugging('[LSP MANAGER] initializeLspServerManager() called')
|
||||
|
||||
// Skip if already initialized or currently initializing
|
||||
if (lspManagerInstance !== undefined && initializationState !== 'failed') {
|
||||
logForDebugging(
|
||||
'[LSP MANAGER] Already initialized or initializing, skipping',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Reset state for retry if previous initialization failed
|
||||
if (initializationState === 'failed') {
|
||||
lspManagerInstance = undefined
|
||||
initializationError = undefined
|
||||
}
|
||||
|
||||
// Create the manager instance and mark as pending
|
||||
lspManagerInstance = createLSPServerManager()
|
||||
initializationState = 'pending'
|
||||
logForDebugging('[LSP MANAGER] Created manager instance, state=pending')
|
||||
|
||||
// Increment generation to invalidate any pending initializations
|
||||
const currentGeneration = ++initializationGeneration
|
||||
logForDebugging(
|
||||
`[LSP MANAGER] Starting async initialization (generation ${currentGeneration})`,
|
||||
)
|
||||
|
||||
// Start initialization asynchronously without blocking
|
||||
// Store the promise so callers can await it via waitForInitialization()
|
||||
initializationPromise = lspManagerInstance
|
||||
.initialize()
|
||||
.then(() => {
|
||||
// Only update state if this is still the current initialization
|
||||
if (currentGeneration === initializationGeneration) {
|
||||
initializationState = 'success'
|
||||
logForDebugging('LSP server manager initialized successfully')
|
||||
|
||||
// Register passive notification handlers for diagnostics
|
||||
if (lspManagerInstance) {
|
||||
registerLSPNotificationHandlers(lspManagerInstance)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
// Only update state if this is still the current initialization
|
||||
if (currentGeneration === initializationGeneration) {
|
||||
initializationState = 'failed'
|
||||
initializationError = error as Error
|
||||
// Clear the instance since it's not usable
|
||||
lspManagerInstance = undefined
|
||||
|
||||
logError(error as Error)
|
||||
logForDebugging(
|
||||
`Failed to initialize LSP server manager: ${errorMessage(error)}`,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Force re-initialization of the LSP server manager, even after a prior
|
||||
* successful init. Called from refreshActivePlugins() after plugin caches
|
||||
* are cleared, so newly-loaded plugin LSP servers are picked up.
|
||||
*
|
||||
* Fixes https://github.com/anthropics/claude-code/issues/15521:
|
||||
* loadAllPlugins() is memoized and can be called very early in startup
|
||||
* (via getCommands prefetch in setup.ts) before marketplaces are reconciled,
|
||||
* caching an empty plugin list. initializeLspServerManager() then reads that
|
||||
* stale memoized result and initializes with 0 servers. Unlike commands/agents/
|
||||
* hooks/MCP, LSP was never re-initialized on plugin refresh.
|
||||
*
|
||||
* Safe to call when no LSP plugins changed: initialize() is just config
|
||||
* parsing (servers are lazy-started on first use). Also safe during pending
|
||||
* init: the generation counter invalidates the in-flight promise.
|
||||
*/
|
||||
export function reinitializeLspServerManager(): void {
|
||||
if (initializationState === 'not-started') {
|
||||
// initializeLspServerManager() was never called (e.g. headless subcommand
|
||||
// path). Don't start it now.
|
||||
return
|
||||
}
|
||||
|
||||
logForDebugging('[LSP MANAGER] reinitializeLspServerManager() called')
|
||||
|
||||
// Best-effort shutdown of any running servers on the old instance so
|
||||
// /reload-plugins doesn't leak child processes. Fire-and-forget: the
|
||||
// primary use case (issue #15521) has 0 servers so this is usually a no-op.
|
||||
if (lspManagerInstance) {
|
||||
void lspManagerInstance.shutdown().catch(err => {
|
||||
logForDebugging(
|
||||
`[LSP MANAGER] old instance shutdown during reinit failed: ${errorMessage(err)}`,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// Force the idempotence check in initializeLspServerManager() to fall
|
||||
// through. Generation counter handles invalidating any in-flight init.
|
||||
lspManagerInstance = undefined
|
||||
initializationState = 'not-started'
|
||||
initializationError = undefined
|
||||
|
||||
initializeLspServerManager()
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown the LSP server manager and clean up resources.
|
||||
*
|
||||
* This should be called during Claude Code shutdown. Stops all running LSP servers
|
||||
* and clears internal state. Safe to call when not initialized (no-op).
|
||||
*
|
||||
* NOTE: Errors during shutdown are logged for monitoring but NOT propagated to the caller.
|
||||
* State is always cleared even if shutdown fails, to prevent resource accumulation.
|
||||
* This is acceptable during application exit when recovery is not possible.
|
||||
*
|
||||
* @returns Promise that resolves when shutdown completes (errors are swallowed)
|
||||
*/
|
||||
export async function shutdownLspServerManager(): Promise<void> {
|
||||
if (lspManagerInstance === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await lspManagerInstance.shutdown()
|
||||
logForDebugging('LSP server manager shut down successfully')
|
||||
} catch (error: unknown) {
|
||||
logError(error as Error)
|
||||
logForDebugging(
|
||||
`Failed to shutdown LSP server manager: ${errorMessage(error)}`,
|
||||
)
|
||||
} finally {
|
||||
// Always clear state even if shutdown failed
|
||||
lspManagerInstance = undefined
|
||||
initializationState = 'not-started'
|
||||
initializationError = undefined
|
||||
initializationPromise = undefined
|
||||
// Increment generation to invalidate any pending initializations
|
||||
initializationGeneration++
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user