Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
11 kB · 341 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342import { join } from 'path'import { getCwd } from '../cwd.js'import { logForDebugging } from '../debug.js'import { logError } from '../log.js'import type { SettingSource } from '../settings/constants.js'import { getInitialSettings, getSettingsForSource, updateSettingsForSource,} from '../settings/settings.js'import { getAddDirEnabledPlugins } from './addDirPluginSettings.js'import { getInMemoryInstalledPlugins, migrateFromEnabledPlugins,} from './installedPluginsManager.js'import { getPluginById } from './marketplaceManager.js'import { type ExtendedPluginScope, type PersistablePluginScope, SETTING_SOURCE_TO_SCOPE, scopeToSettingSource,} from './pluginIdentifier.js'import { cacheAndRegisterPlugin, registerPluginInstallation,} from './pluginInstallationHelpers.js'import { isLocalPluginSource, type PluginScope } from './schemas.js'
/** * Checks for enabled plugins across all settings sources, including --add-dir. * * Uses getInitialSettings() which merges all sources with policy as * highest priority, then layers --add-dir plugins underneath. This is the * authoritative "is this plugin enabled?" check — don't delegate to * getPluginEditableScopes() which serves a different purpose (scope tracking). * * @returns Array of plugin IDs (plugin@marketplace format) that are enabled */export async function checkEnabledPlugins(): Promise<string[]> { const settings = getInitialSettings() const enabledPlugins: string[] = []
// Start with --add-dir plugins (lowest priority) const addDirPlugins = getAddDirEnabledPlugins() for (const [pluginId, value] of Object.entries(addDirPlugins)) { if (pluginId.includes('@') && value) { enabledPlugins.push(pluginId) } }
// Merged settings (policy > local > project > user) override --add-dir if (settings.enabledPlugins) { for (const [pluginId, value] of Object.entries(settings.enabledPlugins)) { if (!pluginId.includes('@')) { continue } const idx = enabledPlugins.indexOf(pluginId) if (value) { if (idx === -1) { enabledPlugins.push(pluginId) } } else { // Explicitly disabled — remove even if --add-dir enabled it if (idx !== -1) { enabledPlugins.splice(idx, 1) } } } }
return enabledPlugins}
/** * Gets the user-editable scope that "owns" each enabled plugin. * * Used for scope tracking: determining where to write back when a user * enables/disables a plugin. Managed (policy) settings are processed first * (lowest priority) because the user cannot edit them — the scope should * resolve to the highest user-controllable source. * * NOTE: This is NOT the authoritative "is this plugin enabled?" check. * Use checkEnabledPlugins() for that — it uses merged settings where * policy has highest priority and can block user-enabled plugins. * * Precedence (lowest to highest): * 0. addDir (--add-dir directories) - session-only, lowest priority * 1. managed (policySettings) - not user-editable * 2. user (userSettings) * 3. project (projectSettings) * 4. local (localSettings) * 5. flag (flagSettings) - session-only, not persisted * * @returns Map of plugin ID to the user-editable scope that owns it */export function getPluginEditableScopes(): Map<string, ExtendedPluginScope> { const result = new Map<string, ExtendedPluginScope>()
// Process --add-dir directories FIRST (lowest priority, overridden by all standard sources) const addDirPlugins = getAddDirEnabledPlugins() for (const [pluginId, value] of Object.entries(addDirPlugins)) { if (!pluginId.includes('@')) { continue } if (value === true) { result.set(pluginId, 'flag') // 'flag' scope = session-only, no write-back } else if (value === false) { result.delete(pluginId) } }
// Process standard sources in precedence order (later overrides earlier) const scopeSources: Array<{ scope: ExtendedPluginScope source: SettingSource }> = [ { scope: 'managed', source: 'policySettings' }, { scope: 'user', source: 'userSettings' }, { scope: 'project', source: 'projectSettings' }, { scope: 'local', source: 'localSettings' }, { scope: 'flag', source: 'flagSettings' }, ]
for (const { scope, source } of scopeSources) { const settings = getSettingsForSource(source) if (!settings?.enabledPlugins) { continue }
for (const [pluginId, value] of Object.entries(settings.enabledPlugins)) { // Skip invalid format if (!pluginId.includes('@')) { continue }
// Log when a standard source overrides an --add-dir plugin if (pluginId in addDirPlugins && addDirPlugins[pluginId] !== value) { logForDebugging( `Plugin ${pluginId} from --add-dir (${addDirPlugins[pluginId]}) overridden by ${source} (${value})`, ) }
if (value === true) { // Plugin enabled at this scope result.set(pluginId, scope) } else if (value === false) { // Explicitly disabled - remove from result result.delete(pluginId) } // Note: Other values (like version strings for future P2) are ignored for now } }
logForDebugging( `Found ${result.size} enabled plugins with scopes: ${Array.from( result.entries(), ) .map(([id, scope]) => `${id}(${scope})`) .join(', ')}`, )
return result}
/** * Check if a scope is persistable (not session-only). * @param scope The scope to check * @returns true if the scope should be persisted to installed_plugins.json */export function isPersistableScope( scope: ExtendedPluginScope,): scope is PersistablePluginScope { return scope !== 'flag'}
/** * Convert SettingSource to plugin scope. * @param source The settings source * @returns The corresponding plugin scope */export function settingSourceToScope( source: SettingSource,): ExtendedPluginScope { return SETTING_SOURCE_TO_SCOPE[source]}
/** * Gets the list of currently installed plugins * Reads from installed_plugins.json which tracks global installation state. * Automatically runs migration on first call if needed. * * Always uses V2 format and initializes the in-memory session state * (which triggers V1→V2 migration if needed). * * @returns Array of installed plugin IDs */export async function getInstalledPlugins(): Promise<string[]> { // Trigger sync in background (don't await - don't block startup) // This syncs enabledPlugins from settings.json to installed_plugins.json void migrateFromEnabledPlugins().catch(error => { logError(error) })
// Always use V2 format - initializes in-memory session state and triggers V1→V2 migration const v2Data = getInMemoryInstalledPlugins() const installed = Object.keys(v2Data.plugins) logForDebugging(`Found ${installed.length} installed plugins`) return installed}
/** * Finds plugins that are enabled but not installed * @param enabledPlugins Array of enabled plugin IDs * @returns Array of missing plugin IDs */export async function findMissingPlugins( enabledPlugins: string[],): Promise<string[]> { try { const installedPlugins = await getInstalledPlugins()
// Filter to not-installed synchronously, then look up all in parallel. // Results are collected in original enabledPlugins order. const notInstalled = enabledPlugins.filter( id => !installedPlugins.includes(id), ) const lookups = await Promise.all( notInstalled.map(async pluginId => { try { const plugin = await getPluginById(pluginId) return { pluginId, found: plugin !== null && plugin !== undefined } } catch (error) { logForDebugging( `Failed to check plugin ${pluginId} in marketplace: ${error}`, ) // Plugin doesn't exist in any marketplace, will be handled as an error return { pluginId, found: false } } }), ) const missing = lookups .filter(({ found }) => found) .map(({ pluginId }) => pluginId)
return missing } catch (error) { logError(error) return [] }}
/** * Result of plugin installation attempt */export type PluginInstallResult = { installed: string[] failed: Array<{ name: string; error: string }>}
/** * Installation scope type for install functions (excludes 'managed' which is read-only) */type InstallableScope = Exclude<PluginScope, 'managed'>
/** * Installs the selected plugins * @param pluginsToInstall Array of plugin IDs to install * @param onProgress Optional callback for installation progress * @param scope Installation scope: user, project, or local (defaults to 'user') * @returns Installation results with succeeded and failed plugins */export async function installSelectedPlugins( pluginsToInstall: string[], onProgress?: (name: string, index: number, total: number) => void, scope: InstallableScope = 'user',): Promise<PluginInstallResult> { // Get projectPath for non-user scopes const projectPath = scope !== 'user' ? getCwd() : undefined
// Get the correct settings source for this scope const settingSource = scopeToSettingSource(scope) const settings = getSettingsForSource(settingSource) const updatedEnabledPlugins = { ...settings?.enabledPlugins } const installed: string[] = [] const failed: Array<{ name: string; error: string }> = []
for (let i = 0; i < pluginsToInstall.length; i++) { const pluginId = pluginsToInstall[i] if (!pluginId) continue
if (onProgress) { onProgress(pluginId, i + 1, pluginsToInstall.length) }
try { const pluginInfo = await getPluginById(pluginId) if (!pluginInfo) { failed.push({ name: pluginId, error: 'Plugin not found in any marketplace', }) continue }
// Cache the plugin if it's from an external source const { entry, marketplaceInstallLocation } = pluginInfo if (!isLocalPluginSource(entry.source)) { // External plugin - cache and register it with scope await cacheAndRegisterPlugin(pluginId, entry, scope, projectPath) } else { // Local plugin - just register it with the install path and scope registerPluginInstallation( { pluginId, installPath: join(marketplaceInstallLocation, entry.source), version: entry.version, }, scope, projectPath, ) }
// Mark as enabled in settings updatedEnabledPlugins[pluginId] = true installed.push(pluginId) } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) failed.push({ name: pluginId, error: errorMessage }) logError(error) } }
// Update settings with newly enabled plugins using the correct settings source updateSettingsForSource(settingSource, { ...settings, enabledPlugins: updatedEnabledPlugins, })
return { installed, failed }}