(pluginId: string)
| 208 | * "uninstall failed" message for a cleanup side-effect. |
| 209 | */ |
| 210 | export function deletePluginOptions(pluginId: string): void { |
| 211 | // Settings side — also wipes the legacy mcpServers sub-key (same story: |
| 212 | // orphaned on uninstall, never cleaned up before this PR). |
| 213 | // |
| 214 | // Use `undefined` (not `delete`) because `updateSettingsForSource` merges |
| 215 | // via `mergeWith` — absent keys are ignored, only `undefined` triggers |
| 216 | // removal. Cast is deliberate (CLAUDE.md's 10% case): adding z.undefined() |
| 217 | // to the schema instead (like enabledPlugins:466 does) leaks |
| 218 | // `| {[k: string]: unknown}` into the public SDK type, which subsumes the |
| 219 | // real object arm and kills excess-property checks for SDK consumers. The |
| 220 | // mergeWith-deletion contract is internal plumbing — it shouldn't shape |
| 221 | // the Zod schema. enabledPlugins gets away with it only because its other |
| 222 | // arms (string[] | boolean) are non-objects that stay distinct. |
| 223 | const settings = getSettings_DEPRECATED() |
| 224 | type PluginConfigs = NonNullable<typeof settings.pluginConfigs> |
| 225 | if (settings.pluginConfigs?.[pluginId]) { |
| 226 | // Partial<Record<K,V>> = Record<K, V | undefined> — gives us the widening |
| 227 | // for the undefined value, and Partial-of-X overlaps with X so the cast |
| 228 | // is a narrowing TS accepts (same approach as marketplaceManager.ts:1795). |
| 229 | const pluginConfigs: Partial<PluginConfigs> = { [pluginId]: undefined } |
| 230 | const { error } = updateSettingsForSource('userSettings', { |
| 231 | pluginConfigs: pluginConfigs as PluginConfigs, |
| 232 | }) |
| 233 | if (error) { |
| 234 | logForDebugging( |
| 235 | `deletePluginOptions: failed to clear settings.pluginConfigs[${pluginId}]: ${error.message}`, |
| 236 | { level: 'warn' }, |
| 237 | ) |
| 238 | } |
| 239 | } |
| 240 | |
| 241 | // Secure storage side — delete both the top-level pluginSecrets[pluginId] |
| 242 | // and any per-server composite keys `${pluginId}/${server}` (from |
| 243 | // saveMcpServerUserConfig's sensitive split). `/` prefix match is safe: |
| 244 | // plugin IDs are `name@marketplace`, never contain `/`, so |
| 245 | // startsWith(`${id}/`) can't false-positive on a different plugin. |
| 246 | const storage = getSecureStorage() |
| 247 | const existing = storage.read() |
| 248 | if (existing?.pluginSecrets) { |
| 249 | const prefix = `${pluginId}/` |
| 250 | const survivingEntries = Object.entries(existing.pluginSecrets).filter( |
| 251 | ([k]) => k !== pluginId && !k.startsWith(prefix), |
| 252 | ) |
| 253 | if ( |
| 254 | survivingEntries.length !== Object.keys(existing.pluginSecrets).length |
| 255 | ) { |
| 256 | const result = storage.update({ |
| 257 | ...existing, |
| 258 | pluginSecrets: |
| 259 | survivingEntries.length > 0 |
| 260 | ? Object.fromEntries(survivingEntries) |
| 261 | : undefined, |
| 262 | }) |
| 263 | if (!result.success) { |
| 264 | logForDebugging( |
| 265 | `deletePluginOptions: failed to clear pluginSecrets for ${pluginId} from keychain`, |
| 266 | { level: 'warn' }, |
| 267 | ) |
no test coverage detected