/** * 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 { 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) } }