PluginInstallationManager.ts
services/plugins/PluginInstallationManager.ts
185
Lines
6017
Bytes
1
Exports
9
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with integrations. It contains 185 lines, 9 detected imports, and 1 detected exports.
Important relationships
Detected exports
performBackgroundPluginInstallations
Keywords
pluginssetappstateprevnamelengthmarketplacesinstalledresultmarketplaceutils
Detected imports
../../state/AppState.js../../utils/debug.js../../utils/diagLogs.js../../utils/log.js../../utils/plugins/marketplaceManager.js../../utils/plugins/pluginLoader.js../../utils/plugins/reconciler.js../../utils/plugins/refresh.js../analytics/index.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
/**
* 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)
}
}