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