Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
9.5 kB · 284 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285/** * Background plugin autoupdate functionality * * At startup, this module: * 1. First updates marketplaces that have autoUpdate enabled * 2. Then checks all installed plugins from those marketplaces and updates them * * Updates are non-inplace (disk-only), requiring a restart to take effect. * Official Anthropic marketplaces have autoUpdate enabled by default, * but users can disable it per-marketplace. */
import { updatePluginOp } from '../../services/plugins/pluginOperations.js'import { shouldSkipPluginAutoupdate } from '../config.js'import { logForDebugging } from '../debug.js'import { errorMessage } from '../errors.js'import { logError } from '../log.js'import { getPendingUpdatesDetails, hasPendingUpdates, isInstallationRelevantToCurrentProject, loadInstalledPluginsFromDisk,} from './installedPluginsManager.js'import { getDeclaredMarketplaces, loadKnownMarketplacesConfig, refreshMarketplace,} from './marketplaceManager.js'import { parsePluginIdentifier } from './pluginIdentifier.js'import { isMarketplaceAutoUpdate, type PluginScope } from './schemas.js'
/** * Callback type for notifying when plugins have been updated */export type PluginAutoUpdateCallback = (updatedPlugins: string[]) => void
// Store callback for plugin update notificationslet pluginUpdateCallback: PluginAutoUpdateCallback | null = null
// Store pending updates that occurred before callback was registered// This handles the race condition where updates complete before REPL mountslet pendingNotification: string[] | null = null
/** * Register a callback to be notified when plugins are auto-updated. * This is used by the REPL to show restart notifications. * * If plugins were already updated before the callback was registered, * the callback will be invoked immediately with the pending updates. */export function onPluginsAutoUpdated( callback: PluginAutoUpdateCallback,): () => void { pluginUpdateCallback = callback
// If there are pending updates that happened before registration, deliver them now if (pendingNotification !== null && pendingNotification.length > 0) { callback(pendingNotification) pendingNotification = null }
return () => { pluginUpdateCallback = null }}
/** * Check if pending updates came from autoupdate (for notification purposes). * Returns the list of plugin names that have pending updates. */export function getAutoUpdatedPluginNames(): string[] { if (!hasPendingUpdates()) { return [] } return getPendingUpdatesDetails().map( d => parsePluginIdentifier(d.pluginId).name, )}
/** * Get the set of marketplaces that have autoUpdate enabled. * Returns the marketplace names that should be auto-updated. */async function getAutoUpdateEnabledMarketplaces(): Promise<Set<string>> { const config = await loadKnownMarketplacesConfig() const declared = getDeclaredMarketplaces() const enabled = new Set<string>()
for (const [name, entry] of Object.entries(config)) { // Settings-declared autoUpdate takes precedence over JSON state const declaredAutoUpdate = declared[name]?.autoUpdate const autoUpdate = declaredAutoUpdate !== undefined ? declaredAutoUpdate : isMarketplaceAutoUpdate(name, entry) if (autoUpdate) { enabled.add(name.toLowerCase()) } }
return enabled}
/** * Update a single plugin's installations. * Returns the plugin ID if any installation was updated, null otherwise. */async function updatePlugin( pluginId: string, installations: Array<{ scope: PluginScope; projectPath?: string }>,): Promise<string | null> { let wasUpdated = false
for (const { scope } of installations) { try { const result = await updatePluginOp(pluginId, scope)
if (result.success && !result.alreadyUpToDate) { wasUpdated = true logForDebugging( `Plugin autoupdate: updated ${pluginId} from ${result.oldVersion} to ${result.newVersion}`, ) } else if (!result.alreadyUpToDate) { logForDebugging( `Plugin autoupdate: failed to update ${pluginId}: ${result.message}`, { level: 'warn' }, ) } } catch (error) { logForDebugging( `Plugin autoupdate: error updating ${pluginId}: ${errorMessage(error)}`, { level: 'warn' }, ) } }
return wasUpdated ? pluginId : null}
/** * Update all project-relevant installed plugins from the given marketplaces. * * Iterates installed_plugins.json, filters to plugins whose marketplace is in * the set, further filters each plugin's installations to those relevant to * the current project (user/managed scope, or project/local scope matching * cwd — see isInstallationRelevantToCurrentProject), then calls updatePluginOp * per installation. Already-up-to-date plugins are silently skipped. * * Called by: * - updatePlugins() below — background autoupdate path (autoUpdate-enabled * marketplaces only; third-party marketplaces default autoUpdate: false) * - ManageMarketplaces.tsx applyChanges() — user-initiated /plugin marketplace * update. Before #29512 this path only called refreshMarketplace() (git * pull on the marketplace clone), so the loader would create the new * version cache dir but installed_plugins.json stayed on the old version, * and the orphan GC stamped the NEW dir with .orphaned_at on next startup. * * @param marketplaceNames - lowercase marketplace names to update plugins from * @returns plugin IDs that were actually updated (not already up-to-date) */export async function updatePluginsForMarketplaces( marketplaceNames: Set<string>,): Promise<string[]> { const installedPlugins = loadInstalledPluginsFromDisk() const pluginIds = Object.keys(installedPlugins.plugins)
if (pluginIds.length === 0) { return [] }
const results = await Promise.allSettled( pluginIds.map(async pluginId => { const { marketplace } = parsePluginIdentifier(pluginId) if (!marketplace || !marketplaceNames.has(marketplace.toLowerCase())) { return null }
const allInstallations = installedPlugins.plugins[pluginId] if (!allInstallations || allInstallations.length === 0) { return null }
const relevantInstallations = allInstallations.filter( isInstallationRelevantToCurrentProject, ) if (relevantInstallations.length === 0) { return null }
return updatePlugin(pluginId, relevantInstallations) }), )
return results .filter( (r): r is PromiseFulfilledResult<string> => r.status === 'fulfilled' && r.value !== null, ) .map(r => r.value)}
/** * Update plugins from marketplaces that have autoUpdate enabled. * Returns the list of plugin IDs that were updated. */async function updatePlugins( autoUpdateEnabledMarketplaces: Set<string>,): Promise<string[]> { return updatePluginsForMarketplaces(autoUpdateEnabledMarketplaces)}
/** * Auto-update marketplaces and plugins in the background. * * This function: * 1. Checks which marketplaces have autoUpdate enabled * 2. Refreshes only those marketplaces (git pull/re-download) * 3. Updates installed plugins from those marketplaces * 4. If any plugins were updated, notifies via the registered callback * * Official Anthropic marketplaces have autoUpdate enabled by default, * but users can disable it per-marketplace in the UI. * * This function runs silently without blocking user interaction. * Called from main.tsx during startup as a background job. */export function autoUpdateMarketplacesAndPluginsInBackground(): void { void (async () => { if (shouldSkipPluginAutoupdate()) { logForDebugging('Plugin autoupdate: skipped (auto-updater disabled)') return }
try { // Get marketplaces with autoUpdate enabled const autoUpdateEnabledMarketplaces = await getAutoUpdateEnabledMarketplaces()
if (autoUpdateEnabledMarketplaces.size === 0) { return }
// Refresh only marketplaces with autoUpdate enabled const refreshResults = await Promise.allSettled( Array.from(autoUpdateEnabledMarketplaces).map(async name => { try { await refreshMarketplace(name, undefined, { disableCredentialHelper: true, }) } catch (error) { logForDebugging( `Plugin autoupdate: failed to refresh marketplace ${name}: ${errorMessage(error)}`, { level: 'warn' }, ) } }), )
// Log any refresh failures const failures = refreshResults.filter(r => r.status === 'rejected') if (failures.length > 0) { logForDebugging( `Plugin autoupdate: ${failures.length} marketplace refresh(es) failed`, { level: 'warn' }, ) }
logForDebugging('Plugin autoupdate: checking installed plugins') const updatedPlugins = await updatePlugins(autoUpdateEnabledMarketplaces)
if (updatedPlugins.length > 0) { if (pluginUpdateCallback) { // Callback is already registered, invoke it immediately pluginUpdateCallback(updatedPlugins) } else { // Callback not yet registered (REPL not mounted), store for later delivery pendingNotification = updatedPlugins } } } catch (error) { logError(error) } })()}