Download src/utils/plugins/pluginAutoupdate.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 9.47 kB
-
https://huggingface.co/chenbhao/codev/resolve/main/src/utils/plugins/pluginAutoupdate.ts
- Command line
-
hf download hf://chenbhao/codev/src/utils/plugins/pluginAutoupdate.ts
-
curl -L -o pluginAutoupdate.ts https://huggingface.co/chenbhao/codev/resolve/main/src/utils/plugins/pluginAutoupdate.ts
9.47 kB
| /** | |
| * 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 notifications | |
| let pluginUpdateCallback: PluginAutoUpdateCallback | null = null | |
| // Store pending updates that occurred before callback was registered | |
| // This handles the race condition where updates complete before REPL mounts | |
| let 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) | |
| } | |
| })() | |
| } | |