chore: initialize recovered claude workspace
This commit is contained in:
184
src/services/plugins/PluginInstallationManager.ts
Normal file
184
src/services/plugins/PluginInstallationManager.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user