source dump of claude code
at main 127 lines 4.4 kB view raw
1/** 2 * Plugin delisting detection. 3 * 4 * Compares installed plugins against marketplace manifests to find plugins 5 * that have been removed, and auto-uninstalls them. 6 * 7 * The security.json fetch was removed (see #25447) — ~29.5M/week GitHub hits 8 * for UI reason/text only. If re-introduced, serve from downloads.claude.ai. 9 */ 10 11import { uninstallPluginOp } from '../../services/plugins/pluginOperations.js' 12import { logForDebugging } from '../debug.js' 13import { errorMessage } from '../errors.js' 14import { loadInstalledPluginsV2 } from './installedPluginsManager.js' 15import { 16 getMarketplace, 17 loadKnownMarketplacesConfigSafe, 18} from './marketplaceManager.js' 19import { 20 addFlaggedPlugin, 21 getFlaggedPlugins, 22 loadFlaggedPlugins, 23} from './pluginFlagging.js' 24import type { InstalledPluginsFileV2, PluginMarketplace } from './schemas.js' 25 26/** 27 * Detect plugins installed from a marketplace that are no longer listed there. 28 * 29 * @param installedPlugins All installed plugins 30 * @param marketplace The marketplace to check against 31 * @param marketplaceName The marketplace name suffix (e.g. "claude-plugins-official") 32 * @returns List of delisted plugin IDs in "name@marketplace" format 33 */ 34export function detectDelistedPlugins( 35 installedPlugins: InstalledPluginsFileV2, 36 marketplace: PluginMarketplace, 37 marketplaceName: string, 38): string[] { 39 const marketplacePluginNames = new Set(marketplace.plugins.map(p => p.name)) 40 const suffix = `@${marketplaceName}` 41 42 const delisted: string[] = [] 43 for (const pluginId of Object.keys(installedPlugins.plugins)) { 44 if (!pluginId.endsWith(suffix)) continue 45 46 const pluginName = pluginId.slice(0, -suffix.length) 47 if (!marketplacePluginNames.has(pluginName)) { 48 delisted.push(pluginId) 49 } 50 } 51 52 return delisted 53} 54 55/** 56 * Detect delisted plugins across all marketplaces, auto-uninstall them, 57 * and record them as flagged. 58 * 59 * This is the core delisting enforcement logic, shared between interactive 60 * mode (useManagePlugins) and headless mode (main.tsx print path). 61 * 62 * @returns List of newly flagged plugin IDs 63 */ 64export async function detectAndUninstallDelistedPlugins(): Promise<string[]> { 65 await loadFlaggedPlugins() 66 67 const installedPlugins = loadInstalledPluginsV2() 68 const alreadyFlagged = getFlaggedPlugins() 69 // Read-only iteration — Safe variant so a corrupted config doesn't throw 70 // out of this function (it's called in the same try-block as loadAllPlugins 71 // in useManagePlugins, so a throw here would void loadAllPlugins' resilience). 72 const knownMarketplaces = await loadKnownMarketplacesConfigSafe() 73 const newlyFlagged: string[] = [] 74 75 for (const marketplaceName of Object.keys(knownMarketplaces)) { 76 try { 77 const marketplace = await getMarketplace(marketplaceName) 78 79 if (!marketplace.forceRemoveDeletedPlugins) continue 80 81 const delisted = detectDelistedPlugins( 82 installedPlugins, 83 marketplace, 84 marketplaceName, 85 ) 86 87 for (const pluginId of delisted) { 88 if (pluginId in alreadyFlagged) continue 89 90 // Skip managed-only plugins — enterprise admin should handle those 91 const installations = installedPlugins.plugins[pluginId] ?? [] 92 const hasUserInstall = installations.some( 93 i => 94 i.scope === 'user' || i.scope === 'project' || i.scope === 'local', 95 ) 96 if (!hasUserInstall) continue 97 98 // Auto-uninstall the delisted plugin from all user-controllable scopes 99 for (const installation of installations) { 100 const { scope } = installation 101 if (scope !== 'user' && scope !== 'project' && scope !== 'local') { 102 continue 103 } 104 try { 105 await uninstallPluginOp(pluginId, scope) 106 } catch (error) { 107 logForDebugging( 108 `Failed to auto-uninstall delisted plugin ${pluginId} from ${scope}: ${errorMessage(error)}`, 109 { level: 'error' }, 110 ) 111 } 112 } 113 114 await addFlaggedPlugin(pluginId) 115 newlyFlagged.push(pluginId) 116 } 117 } catch (error) { 118 // Marketplace may not be available yet — log and continue 119 logForDebugging( 120 `Failed to check for delisted plugins in "${marketplaceName}": ${errorMessage(error)}`, 121 { level: 'warn' }, 122 ) 123 } 124 } 125 126 return newlyFlagged 127}