π File detail
utils/plugins/pluginAutoupdate.ts
π― Use case
This file lives under βutils/β, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, β¦). On the API surface it exposes PluginAutoUpdateCallback, onPluginsAutoUpdated, getAutoUpdatedPluginNames, updatePluginsForMarketplaces, and autoUpdateMarketplacesAndPluginsInBackground β mainly functions, hooks, or classes. It composes internal code from services, config, debug, errors, and log (relative imports). What the file header says: 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. Of.
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
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.
π€ Exports (heuristic)
PluginAutoUpdateCallbackonPluginsAutoUpdatedgetAutoUpdatedPluginNamesupdatePluginsForMarketplacesautoUpdateMarketplacesAndPluginsInBackground
π₯οΈ Source preview
/**
* 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)
}
})()
}