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