File size: 6,017 Bytes
064bfd6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | /**
* 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)
}
}
|