(sources: {
session: LoadedPlugin[]
marketplace: LoadedPlugin[]
builtin: LoadedPlugin[]
managedNames?: Set<string> | null
})
| 3007 | * installed plugin. |
| 3008 | */ |
| 3009 | export function mergePluginSources(sources: { |
| 3010 | session: LoadedPlugin[] |
| 3011 | marketplace: LoadedPlugin[] |
| 3012 | builtin: LoadedPlugin[] |
| 3013 | managedNames?: Set<string> | null |
| 3014 | }): { plugins: LoadedPlugin[]; errors: PluginError[] } { |
| 3015 | const errors: PluginError[] = [] |
| 3016 | const managed = sources.managedNames |
| 3017 | |
| 3018 | // Managed settings win over --plugin-dir. Drop session plugins whose |
| 3019 | // name appears in policySettings.enabledPlugins (whether force-enabled |
| 3020 | // OR force-disabled — both are admin intent that --plugin-dir must not |
| 3021 | // bypass). Surface an error so the user knows why their dev copy was |
| 3022 | // ignored. |
| 3023 | // |
| 3024 | // NOTE: managedNames contains the pluginId prefix (entry.name), which is |
| 3025 | // expected to equal manifest.name by convention (schema description at |
| 3026 | // schemas.ts PluginMarketplaceEntry.name). If a marketplace publishes a |
| 3027 | // plugin where entry.name ≠ manifest.name, this guard will silently miss — |
| 3028 | // but that's a marketplace misconfiguration that breaks other things too |
| 3029 | // (e.g., ManagePlugins constructs pluginIds from manifest.name). |
| 3030 | const sessionPlugins = sources.session.filter(p => { |
| 3031 | if (managed?.has(p.name)) { |
| 3032 | logForDebugging( |
| 3033 | `Plugin "${p.name}" from --plugin-dir is blocked by managed settings`, |
| 3034 | { level: 'warn' }, |
| 3035 | ) |
| 3036 | errors.push({ |
| 3037 | type: 'generic-error', |
| 3038 | source: p.source, |
| 3039 | plugin: p.name, |
| 3040 | error: `--plugin-dir copy of "${p.name}" ignored: plugin is locked by managed settings`, |
| 3041 | }) |
| 3042 | return false |
| 3043 | } |
| 3044 | return true |
| 3045 | }) |
| 3046 | |
| 3047 | const sessionNames = new Set(sessionPlugins.map(p => p.name)) |
| 3048 | const marketplacePlugins = sources.marketplace.filter(p => { |
| 3049 | if (sessionNames.has(p.name)) { |
| 3050 | logForDebugging( |
| 3051 | `Plugin "${p.name}" from --plugin-dir overrides installed version`, |
| 3052 | ) |
| 3053 | return false |
| 3054 | } |
| 3055 | return true |
| 3056 | }) |
| 3057 | // Session first, then non-overridden marketplace, then builtin. |
| 3058 | // Downstream first-match consumers see session plugins before |
| 3059 | // installed ones for any that slipped past the name filter. |
| 3060 | return { |
| 3061 | plugins: [...sessionPlugins, ...marketplacePlugins, ...sources.builtin], |
| 3062 | errors, |
| 3063 | } |
| 3064 | } |
| 3065 | |
| 3066 | /** |
no test coverage detected