185 lines
5.9 KiB
TypeScript
185 lines
5.9 KiB
TypeScript
/**
|
|
* 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)
|
|
}
|
|
}
|