source dump of claude code
at main 284 lines 9.5 kB view raw
1/** 2 * Background plugin autoupdate functionality 3 * 4 * At startup, this module: 5 * 1. First updates marketplaces that have autoUpdate enabled 6 * 2. Then checks all installed plugins from those marketplaces and updates them 7 * 8 * Updates are non-inplace (disk-only), requiring a restart to take effect. 9 * Official Anthropic marketplaces have autoUpdate enabled by default, 10 * but users can disable it per-marketplace. 11 */ 12 13import { updatePluginOp } from '../../services/plugins/pluginOperations.js' 14import { shouldSkipPluginAutoupdate } from '../config.js' 15import { logForDebugging } from '../debug.js' 16import { errorMessage } from '../errors.js' 17import { logError } from '../log.js' 18import { 19 getPendingUpdatesDetails, 20 hasPendingUpdates, 21 isInstallationRelevantToCurrentProject, 22 loadInstalledPluginsFromDisk, 23} from './installedPluginsManager.js' 24import { 25 getDeclaredMarketplaces, 26 loadKnownMarketplacesConfig, 27 refreshMarketplace, 28} from './marketplaceManager.js' 29import { parsePluginIdentifier } from './pluginIdentifier.js' 30import { isMarketplaceAutoUpdate, type PluginScope } from './schemas.js' 31 32/** 33 * Callback type for notifying when plugins have been updated 34 */ 35export type PluginAutoUpdateCallback = (updatedPlugins: string[]) => void 36 37// Store callback for plugin update notifications 38let pluginUpdateCallback: PluginAutoUpdateCallback | null = null 39 40// Store pending updates that occurred before callback was registered 41// This handles the race condition where updates complete before REPL mounts 42let pendingNotification: string[] | null = null 43 44/** 45 * Register a callback to be notified when plugins are auto-updated. 46 * This is used by the REPL to show restart notifications. 47 * 48 * If plugins were already updated before the callback was registered, 49 * the callback will be invoked immediately with the pending updates. 50 */ 51export function onPluginsAutoUpdated( 52 callback: PluginAutoUpdateCallback, 53): () => void { 54 pluginUpdateCallback = callback 55 56 // If there are pending updates that happened before registration, deliver them now 57 if (pendingNotification !== null && pendingNotification.length > 0) { 58 callback(pendingNotification) 59 pendingNotification = null 60 } 61 62 return () => { 63 pluginUpdateCallback = null 64 } 65} 66 67/** 68 * Check if pending updates came from autoupdate (for notification purposes). 69 * Returns the list of plugin names that have pending updates. 70 */ 71export function getAutoUpdatedPluginNames(): string[] { 72 if (!hasPendingUpdates()) { 73 return [] 74 } 75 return getPendingUpdatesDetails().map( 76 d => parsePluginIdentifier(d.pluginId).name, 77 ) 78} 79 80/** 81 * Get the set of marketplaces that have autoUpdate enabled. 82 * Returns the marketplace names that should be auto-updated. 83 */ 84async function getAutoUpdateEnabledMarketplaces(): Promise<Set<string>> { 85 const config = await loadKnownMarketplacesConfig() 86 const declared = getDeclaredMarketplaces() 87 const enabled = new Set<string>() 88 89 for (const [name, entry] of Object.entries(config)) { 90 // Settings-declared autoUpdate takes precedence over JSON state 91 const declaredAutoUpdate = declared[name]?.autoUpdate 92 const autoUpdate = 93 declaredAutoUpdate !== undefined 94 ? declaredAutoUpdate 95 : isMarketplaceAutoUpdate(name, entry) 96 if (autoUpdate) { 97 enabled.add(name.toLowerCase()) 98 } 99 } 100 101 return enabled 102} 103 104/** 105 * Update a single plugin's installations. 106 * Returns the plugin ID if any installation was updated, null otherwise. 107 */ 108async function updatePlugin( 109 pluginId: string, 110 installations: Array<{ scope: PluginScope; projectPath?: string }>, 111): Promise<string | null> { 112 let wasUpdated = false 113 114 for (const { scope } of installations) { 115 try { 116 const result = await updatePluginOp(pluginId, scope) 117 118 if (result.success && !result.alreadyUpToDate) { 119 wasUpdated = true 120 logForDebugging( 121 `Plugin autoupdate: updated ${pluginId} from ${result.oldVersion} to ${result.newVersion}`, 122 ) 123 } else if (!result.alreadyUpToDate) { 124 logForDebugging( 125 `Plugin autoupdate: failed to update ${pluginId}: ${result.message}`, 126 { level: 'warn' }, 127 ) 128 } 129 } catch (error) { 130 logForDebugging( 131 `Plugin autoupdate: error updating ${pluginId}: ${errorMessage(error)}`, 132 { level: 'warn' }, 133 ) 134 } 135 } 136 137 return wasUpdated ? pluginId : null 138} 139 140/** 141 * Update all project-relevant installed plugins from the given marketplaces. 142 * 143 * Iterates installed_plugins.json, filters to plugins whose marketplace is in 144 * the set, further filters each plugin's installations to those relevant to 145 * the current project (user/managed scope, or project/local scope matching 146 * cwd — see isInstallationRelevantToCurrentProject), then calls updatePluginOp 147 * per installation. Already-up-to-date plugins are silently skipped. 148 * 149 * Called by: 150 * - updatePlugins() below — background autoupdate path (autoUpdate-enabled 151 * marketplaces only; third-party marketplaces default autoUpdate: false) 152 * - ManageMarketplaces.tsx applyChanges() — user-initiated /plugin marketplace 153 * update. Before #29512 this path only called refreshMarketplace() (git 154 * pull on the marketplace clone), so the loader would create the new 155 * version cache dir but installed_plugins.json stayed on the old version, 156 * and the orphan GC stamped the NEW dir with .orphaned_at on next startup. 157 * 158 * @param marketplaceNames - lowercase marketplace names to update plugins from 159 * @returns plugin IDs that were actually updated (not already up-to-date) 160 */ 161export async function updatePluginsForMarketplaces( 162 marketplaceNames: Set<string>, 163): Promise<string[]> { 164 const installedPlugins = loadInstalledPluginsFromDisk() 165 const pluginIds = Object.keys(installedPlugins.plugins) 166 167 if (pluginIds.length === 0) { 168 return [] 169 } 170 171 const results = await Promise.allSettled( 172 pluginIds.map(async pluginId => { 173 const { marketplace } = parsePluginIdentifier(pluginId) 174 if (!marketplace || !marketplaceNames.has(marketplace.toLowerCase())) { 175 return null 176 } 177 178 const allInstallations = installedPlugins.plugins[pluginId] 179 if (!allInstallations || allInstallations.length === 0) { 180 return null 181 } 182 183 const relevantInstallations = allInstallations.filter( 184 isInstallationRelevantToCurrentProject, 185 ) 186 if (relevantInstallations.length === 0) { 187 return null 188 } 189 190 return updatePlugin(pluginId, relevantInstallations) 191 }), 192 ) 193 194 return results 195 .filter( 196 (r): r is PromiseFulfilledResult<string> => 197 r.status === 'fulfilled' && r.value !== null, 198 ) 199 .map(r => r.value) 200} 201 202/** 203 * Update plugins from marketplaces that have autoUpdate enabled. 204 * Returns the list of plugin IDs that were updated. 205 */ 206async function updatePlugins( 207 autoUpdateEnabledMarketplaces: Set<string>, 208): Promise<string[]> { 209 return updatePluginsForMarketplaces(autoUpdateEnabledMarketplaces) 210} 211 212/** 213 * Auto-update marketplaces and plugins in the background. 214 * 215 * This function: 216 * 1. Checks which marketplaces have autoUpdate enabled 217 * 2. Refreshes only those marketplaces (git pull/re-download) 218 * 3. Updates installed plugins from those marketplaces 219 * 4. If any plugins were updated, notifies via the registered callback 220 * 221 * Official Anthropic marketplaces have autoUpdate enabled by default, 222 * but users can disable it per-marketplace in the UI. 223 * 224 * This function runs silently without blocking user interaction. 225 * Called from main.tsx during startup as a background job. 226 */ 227export function autoUpdateMarketplacesAndPluginsInBackground(): void { 228 void (async () => { 229 if (shouldSkipPluginAutoupdate()) { 230 logForDebugging('Plugin autoupdate: skipped (auto-updater disabled)') 231 return 232 } 233 234 try { 235 // Get marketplaces with autoUpdate enabled 236 const autoUpdateEnabledMarketplaces = 237 await getAutoUpdateEnabledMarketplaces() 238 239 if (autoUpdateEnabledMarketplaces.size === 0) { 240 return 241 } 242 243 // Refresh only marketplaces with autoUpdate enabled 244 const refreshResults = await Promise.allSettled( 245 Array.from(autoUpdateEnabledMarketplaces).map(async name => { 246 try { 247 await refreshMarketplace(name, undefined, { 248 disableCredentialHelper: true, 249 }) 250 } catch (error) { 251 logForDebugging( 252 `Plugin autoupdate: failed to refresh marketplace ${name}: ${errorMessage(error)}`, 253 { level: 'warn' }, 254 ) 255 } 256 }), 257 ) 258 259 // Log any refresh failures 260 const failures = refreshResults.filter(r => r.status === 'rejected') 261 if (failures.length > 0) { 262 logForDebugging( 263 `Plugin autoupdate: ${failures.length} marketplace refresh(es) failed`, 264 { level: 'warn' }, 265 ) 266 } 267 268 logForDebugging('Plugin autoupdate: checking installed plugins') 269 const updatedPlugins = await updatePlugins(autoUpdateEnabledMarketplaces) 270 271 if (updatedPlugins.length > 0) { 272 if (pluginUpdateCallback) { 273 // Callback is already registered, invoke it immediately 274 pluginUpdateCallback(updatedPlugins) 275 } else { 276 // Callback not yet registered (REPL not mounted), store for later delivery 277 pendingNotification = updatedPlugins 278 } 279 } 280 } catch (error) { 281 logError(error) 282 } 283 })() 284}