init claude-code
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* Layer-3 refresh primitive: swap active plugin components in the running session.
|
||||
*
|
||||
* Three-layer model (see reconciler.ts for Layer-2):
|
||||
* - Layer 1: intent (settings)
|
||||
* - Layer 2: materialization (~/.claude/plugins/) — reconcileMarketplaces()
|
||||
* - Layer 3: active components (AppState) — this file
|
||||
*
|
||||
* Called from:
|
||||
* - /reload-plugins command (interactive, user-initiated)
|
||||
* - print.ts refreshPluginState() (headless, auto before first query with SYNC_PLUGIN_INSTALL)
|
||||
* - performBackgroundPluginInstallations() (background, auto after new marketplace install)
|
||||
*
|
||||
* NOT called from:
|
||||
* - useManagePlugins needsRefresh effect — interactive mode shows a notification;
|
||||
* user explicitly runs /reload-plugins (PR 5c)
|
||||
* - /plugin menu — sets needsRefresh, user runs /reload-plugins (PR 5b)
|
||||
*/
|
||||
|
||||
import { getOriginalCwd } from '../../bootstrap/state.js'
|
||||
import type { Command } from '../../commands.js'
|
||||
import { reinitializeLspServerManager } from '../../services/lsp/manager.js'
|
||||
import type { AppState } from '../../state/AppState.js'
|
||||
import type { AgentDefinitionsResult } from '../../tools/AgentTool/loadAgentsDir.js'
|
||||
import { getAgentDefinitionsWithOverrides } from '../../tools/AgentTool/loadAgentsDir.js'
|
||||
import type { PluginError } from '../../types/plugin.js'
|
||||
import { logForDebugging } from '../debug.js'
|
||||
import { errorMessage } from '../errors.js'
|
||||
import { logError } from '../log.js'
|
||||
import { clearAllCaches } from './cacheUtils.js'
|
||||
import { getPluginCommands } from './loadPluginCommands.js'
|
||||
import { loadPluginHooks } from './loadPluginHooks.js'
|
||||
import { loadPluginLspServers } from './lspPluginIntegration.js'
|
||||
import { loadPluginMcpServers } from './mcpPluginIntegration.js'
|
||||
import { clearPluginCacheExclusions } from './orphanedPluginFilter.js'
|
||||
import { loadAllPlugins } from './pluginLoader.js'
|
||||
|
||||
type SetAppState = (updater: (prev: AppState) => AppState) => void
|
||||
|
||||
export type RefreshActivePluginsResult = {
|
||||
enabled_count: number
|
||||
disabled_count: number
|
||||
command_count: number
|
||||
agent_count: number
|
||||
hook_count: number
|
||||
mcp_count: number
|
||||
/** LSP servers provided by enabled plugins. reinitializeLspServerManager()
|
||||
* is called unconditionally so the manager picks these up (no-op if
|
||||
* manager was never initialized). */
|
||||
lsp_count: number
|
||||
error_count: number
|
||||
/** The refreshed agent definitions, for callers (e.g. print.ts) that also
|
||||
* maintain a local mutable reference outside AppState. */
|
||||
agentDefinitions: AgentDefinitionsResult
|
||||
/** The refreshed plugin commands, same rationale as agentDefinitions. */
|
||||
pluginCommands: Command[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh all active plugin components: commands, agents, hooks, MCP-reconnect
|
||||
* trigger, AppState plugin arrays. Clears ALL plugin caches (unlike the old
|
||||
* needsRefresh path which only cleared loadAllPlugins and returned stale data
|
||||
* from downstream memoized loaders).
|
||||
*
|
||||
* Consumes plugins.needsRefresh (sets to false).
|
||||
* Increments mcp.pluginReconnectKey so useManageMCPConnections effects re-run
|
||||
* and pick up new plugin MCP servers.
|
||||
*
|
||||
* LSP: if plugins now contribute LSP servers, reinitializeLspServerManager()
|
||||
* re-reads config. Servers are lazy-started so this is just config parsing.
|
||||
*/
|
||||
export async function refreshActivePlugins(
|
||||
setAppState: SetAppState,
|
||||
): Promise<RefreshActivePluginsResult> {
|
||||
logForDebugging('refreshActivePlugins: clearing all plugin caches')
|
||||
clearAllCaches()
|
||||
// Orphan exclusions are session-frozen by default, but /reload-plugins is
|
||||
// an explicit "disk changed, re-read it" signal — recompute them too.
|
||||
clearPluginCacheExclusions()
|
||||
|
||||
// Sequence the full load before cache-only consumers. Before #23693 all
|
||||
// three shared loadAllPlugins()'s memoize promise so Promise.all was a
|
||||
// no-op race. After #23693 getPluginCommands/getAgentDefinitions call
|
||||
// loadAllPluginsCacheOnly (separate memoize) — racing them means they
|
||||
// read installed_plugins.json before loadAllPlugins() has cloned+cached
|
||||
// the plugin, returning plugin-cache-miss. loadAllPlugins warms the
|
||||
// cache-only memoize on completion, so the awaits below are ~free.
|
||||
const pluginResult = await loadAllPlugins()
|
||||
const [pluginCommands, agentDefinitions] = await Promise.all([
|
||||
getPluginCommands(),
|
||||
getAgentDefinitionsWithOverrides(getOriginalCwd()),
|
||||
])
|
||||
|
||||
const { enabled, disabled, errors } = pluginResult
|
||||
|
||||
// Populate mcpServers/lspServers on each enabled plugin. These are lazy
|
||||
// cache slots NOT filled by loadAllPlugins() — they're written later by
|
||||
// extractMcpServersFromPlugins/getPluginLspServers, which races with this.
|
||||
// Loading here gives accurate metrics AND warms the cache slots so the MCP
|
||||
// connection manager (triggered by pluginReconnectKey bump) sees the servers
|
||||
// without re-parsing manifests. Errors are pushed to the shared errors array.
|
||||
const [mcpCounts, lspCounts] = await Promise.all([
|
||||
Promise.all(
|
||||
enabled.map(async p => {
|
||||
if (p.mcpServers) return Object.keys(p.mcpServers).length
|
||||
const servers = await loadPluginMcpServers(p, errors)
|
||||
if (servers) p.mcpServers = servers
|
||||
return servers ? Object.keys(servers).length : 0
|
||||
}),
|
||||
),
|
||||
Promise.all(
|
||||
enabled.map(async p => {
|
||||
if (p.lspServers) return Object.keys(p.lspServers).length
|
||||
const servers = await loadPluginLspServers(p, errors)
|
||||
if (servers) p.lspServers = servers
|
||||
return servers ? Object.keys(servers).length : 0
|
||||
}),
|
||||
),
|
||||
])
|
||||
const mcp_count = mcpCounts.reduce((sum, n) => sum + n, 0)
|
||||
const lsp_count = lspCounts.reduce((sum, n) => sum + n, 0)
|
||||
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
plugins: {
|
||||
...prev.plugins,
|
||||
enabled,
|
||||
disabled,
|
||||
commands: pluginCommands,
|
||||
errors: mergePluginErrors(prev.plugins.errors, errors),
|
||||
needsRefresh: false,
|
||||
},
|
||||
agentDefinitions,
|
||||
mcp: {
|
||||
...prev.mcp,
|
||||
pluginReconnectKey: prev.mcp.pluginReconnectKey + 1,
|
||||
},
|
||||
}))
|
||||
|
||||
// Re-initialize LSP manager so newly-loaded plugin LSP servers are picked
|
||||
// up. No-op if LSP was never initialized (headless subcommand path).
|
||||
// Unconditional so removing the last LSP plugin also clears stale config.
|
||||
// Fixes issue #15521: LSP manager previously read a stale memoized
|
||||
// loadAllPlugins() result from before marketplaces were reconciled.
|
||||
reinitializeLspServerManager()
|
||||
|
||||
// clearAllCaches() prunes removed-plugin hooks; this does the FULL swap
|
||||
// (adds hooks from newly-enabled plugins too). Catching here so
|
||||
// hook_load_failed can feed error_count; a failure doesn't lose the
|
||||
// plugin/command/agent data above (hooks go to STATE.registeredHooks, not
|
||||
// AppState).
|
||||
let hook_load_failed = false
|
||||
try {
|
||||
await loadPluginHooks()
|
||||
} catch (e) {
|
||||
hook_load_failed = true
|
||||
logError(e)
|
||||
logForDebugging(
|
||||
`refreshActivePlugins: loadPluginHooks failed: ${errorMessage(e)}`,
|
||||
)
|
||||
}
|
||||
|
||||
const hook_count = enabled.reduce((sum, p) => {
|
||||
if (!p.hooksConfig) return sum
|
||||
return (
|
||||
sum +
|
||||
Object.values(p.hooksConfig).reduce(
|
||||
(s, matchers) =>
|
||||
s + (matchers?.reduce((h, m) => h + m.hooks.length, 0) ?? 0),
|
||||
0,
|
||||
)
|
||||
)
|
||||
}, 0)
|
||||
|
||||
logForDebugging(
|
||||
`refreshActivePlugins: ${enabled.length} enabled, ${pluginCommands.length} commands, ${agentDefinitions.allAgents.length} agents, ${hook_count} hooks, ${mcp_count} MCP, ${lsp_count} LSP`,
|
||||
)
|
||||
|
||||
return {
|
||||
enabled_count: enabled.length,
|
||||
disabled_count: disabled.length,
|
||||
command_count: pluginCommands.length,
|
||||
agent_count: agentDefinitions.allAgents.length,
|
||||
hook_count,
|
||||
mcp_count,
|
||||
lsp_count,
|
||||
error_count: errors.length + (hook_load_failed ? 1 : 0),
|
||||
agentDefinitions,
|
||||
pluginCommands,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge fresh plugin-load errors with existing errors, preserving LSP and
|
||||
* plugin-component errors that were recorded by other systems and
|
||||
* deduplicating. Same logic as refreshPlugins()/updatePluginState(), extracted
|
||||
* so refresh.ts doesn't leave those errors stranded.
|
||||
*/
|
||||
function mergePluginErrors(
|
||||
existing: PluginError[],
|
||||
fresh: PluginError[],
|
||||
): PluginError[] {
|
||||
const preserved = existing.filter(
|
||||
e => e.source === 'lsp-manager' || e.source.startsWith('plugin:'),
|
||||
)
|
||||
const freshKeys = new Set(fresh.map(errorKey))
|
||||
const deduped = preserved.filter(e => !freshKeys.has(errorKey(e)))
|
||||
return [...deduped, ...fresh]
|
||||
}
|
||||
|
||||
function errorKey(e: PluginError): string {
|
||||
return e.type === 'generic-error'
|
||||
? `generic-error:${e.source}:${e.error}`
|
||||
: `${e.type}:${e.source}`
|
||||
}
|
||||
Reference in New Issue
Block a user