π File detail
services/plugins/PluginInstallationManager.ts
π― Use case
This file lives under βservices/β, which covers long-lived services (LSP, MCP, OAuth, tool execution, memory, compaction, voice, settings sync, β¦). On the API surface it exposes performBackgroundPluginInstallations β mainly functions, hooks, or classes. It composes internal code from state, utils, and analytics (relative imports). What the file header says: Background plugin and marketplace installation manager This module handles automatic installation of plugins and marketplaces from trusted sources (repository and user settings) without blocking startup.
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
Background plugin and marketplace installation manager This module handles automatic installation of plugins and marketplaces from trusted sources (repository and user settings) without blocking startup.
π€ Exports (heuristic)
performBackgroundPluginInstallations
π₯οΈ Source preview
/**
* Background plugin and marketplace installation manager
*
* This module handles automatic installation of plugins and marketplaces
* from trusted sources (repository and user settings) without blocking startup.
*/
import type { AppState } from '../../state/AppState.js'
import { logForDebugging } from '../../utils/debug.js'
import { logForDiagnosticsNoPII } from '../../utils/diagLogs.js'
import { logError } from '../../utils/log.js'
import {
clearMarketplacesCache,
getDeclaredMarketplaces,
loadKnownMarketplacesConfig,
} from '../../utils/plugins/marketplaceManager.js'
import { clearPluginCache } from '../../utils/plugins/pluginLoader.js'
import {
diffMarketplaces,
reconcileMarketplaces,
} from '../../utils/plugins/reconciler.js'
import { refreshActivePlugins } from '../../utils/plugins/refresh.js'
import { logEvent } from '../analytics/index.js'
type SetAppState = (f: (prevState: AppState) => AppState) => void
/**
* Update marketplace installation status in app state
*/
function updateMarketplaceStatus(
setAppState: SetAppState,
name: string,
status: 'pending' | 'installing' | 'installed' | 'failed',
error?: string,
): void {
setAppState(prevState => ({
...prevState,
plugins: {
...prevState.plugins,
installationStatus: {
...prevState.plugins.installationStatus,
marketplaces: prevState.plugins.installationStatus.marketplaces.map(
m => (m.name === name ? { ...m, status, error } : m),
),
},
},
}))
}
/**
* Perform background plugin startup checks and installations.
*
* This is a thin wrapper around reconcileMarketplaces() that maps onProgress
* events to AppState updates for the REPL UI. After marketplaces are
* reconciled:
* - New installs β auto-refresh plugins (fixes "plugin-not-found" errors
* from the initial cache-only load on fresh homespace/cleared cache)
* - Updates only β set needsRefresh, show notification for /reload-plugins
*/
export async function performBackgroundPluginInstallations(
setAppState: SetAppState,
): Promise<void> {
logForDebugging('performBackgroundPluginInstallations called')
try {
// Compute diff upfront for initial UI status (pending spinners)
const declared = getDeclaredMarketplaces()
const materialized = await loadKnownMarketplacesConfig().catch(() => ({}))
const diff = diffMarketplaces(declared, materialized)
const pendingNames = [
...diff.missing,
...diff.sourceChanged.map(c => c.name),
]
// Initialize AppState with pending status. No per-plugin pending status β
// plugin load is fast (cache hit or local copy); marketplace clone is the
// slow part worth showing progress for.
setAppState(prev => ({
...prev,
plugins: {
...prev.plugins,
installationStatus: {
marketplaces: pendingNames.map(name => ({
name,
status: 'pending' as const,
})),
plugins: [],
},
},
}))
if (pendingNames.length === 0) {
return
}
logForDebugging(
`Installing ${pendingNames.length} marketplace(s) in background`,
)
const result = await reconcileMarketplaces({
onProgress: event => {
switch (event.type) {
case 'installing':
updateMarketplaceStatus(setAppState, event.name, 'installing')
break
case 'installed':
updateMarketplaceStatus(setAppState, event.name, 'installed')
break
case 'failed':
updateMarketplaceStatus(
setAppState,
event.name,
'failed',
event.error,
)
break
}
},
})
const metrics = {
installed_count: result.installed.length,
updated_count: result.updated.length,
failed_count: result.failed.length,
up_to_date_count: result.upToDate.length,
}
logEvent('tengu_marketplace_background_install', metrics)
logForDiagnosticsNoPII(
'info',
'tengu_marketplace_background_install',
metrics,
)
if (result.installed.length > 0) {
// New marketplaces were installed β auto-refresh plugins. This fixes
// "Plugin not found in marketplace" errors from the initial cache-only
// load (e.g., fresh homespace where marketplace cache was empty).
// refreshActivePlugins clears all caches, reloads plugins, and bumps
// pluginReconnectKey so MCP connections are re-established.
clearMarketplacesCache()
logForDebugging(
`Auto-refreshing plugins after ${result.installed.length} new marketplace(s) installed`,
)
try {
await refreshActivePlugins(setAppState)
} catch (refreshError) {
// If auto-refresh fails, fall back to needsRefresh notification so
// the user can manually run /reload-plugins to recover.
logError(refreshError)
logForDebugging(
`Auto-refresh failed, falling back to needsRefresh: ${refreshError}`,
{ level: 'warn' },
)
clearPluginCache(
'performBackgroundPluginInstallations: auto-refresh failed',
)
setAppState(prev => {
if (prev.plugins.needsRefresh) return prev
return {
...prev,
plugins: { ...prev.plugins, needsRefresh: true },
}
})
}
} else if (result.updated.length > 0) {
// Existing marketplaces updated β notify user to run /reload-plugins.
// Updates are less urgent and the user should choose when to apply them.
clearMarketplacesCache()
clearPluginCache(
'performBackgroundPluginInstallations: marketplaces reconciled',
)
setAppState(prev => {
if (prev.plugins.needsRefresh) return prev
return {
...prev,
plugins: { ...prev.plugins, needsRefresh: true },
}
})
}
} catch (error) {
logError(error)
}
}