({
pluginId,
entry,
scope,
marketplaceInstallLocation,
}: {
pluginId: string
entry: PluginMarketplaceEntry
scope: 'user' | 'project' | 'local'
marketplaceInstallLocation?: string
})
| 346 | * (from a prior marketplace search) to avoid a redundant lookup. |
| 347 | */ |
| 348 | export async function installResolvedPlugin({ |
| 349 | pluginId, |
| 350 | entry, |
| 351 | scope, |
| 352 | marketplaceInstallLocation, |
| 353 | }: { |
| 354 | pluginId: string |
| 355 | entry: PluginMarketplaceEntry |
| 356 | scope: 'user' | 'project' | 'local' |
| 357 | marketplaceInstallLocation?: string |
| 358 | }): Promise<InstallCoreResult> { |
| 359 | const settingSource = scopeToSettingSource(scope) |
| 360 | |
| 361 | // ── Policy guard ── |
| 362 | // Org-blocked plugins (managed-settings.json enabledPlugins: false) cannot |
| 363 | // be installed. Checked here so all install paths (CLI, UI, hint-triggered) |
| 364 | // are covered in one place. |
| 365 | if (isPluginBlockedByPolicy(pluginId)) { |
| 366 | return { ok: false, reason: 'blocked-by-policy', pluginName: entry.name } |
| 367 | } |
| 368 | |
| 369 | // ── Resolve dependency closure ── |
| 370 | // depInfo caches marketplace lookups so the materialize loop doesn't |
| 371 | // re-fetch. Seed the root if the caller gave us its install location. |
| 372 | const depInfo = new Map< |
| 373 | string, |
| 374 | { entry: PluginMarketplaceEntry; marketplaceInstallLocation: string } |
| 375 | >() |
| 376 | // Without this guard, a local-source root with undefined |
| 377 | // marketplaceInstallLocation falls through: depInfo isn't seeded, the |
| 378 | // materialize loop's `if (!info) continue` skips the root, and the user |
| 379 | // sees "Successfully installed" while nothing is cached. |
| 380 | if (isLocalPluginSource(entry.source) && !marketplaceInstallLocation) { |
| 381 | return { |
| 382 | ok: false, |
| 383 | reason: 'local-source-no-location', |
| 384 | pluginName: entry.name, |
| 385 | } |
| 386 | } |
| 387 | if (marketplaceInstallLocation) { |
| 388 | depInfo.set(pluginId, { entry, marketplaceInstallLocation }) |
| 389 | } |
| 390 | |
| 391 | const rootMarketplace = parsePluginIdentifier(pluginId).marketplace |
| 392 | const allowedCrossMarketplaces = new Set( |
| 393 | (rootMarketplace |
| 394 | ? (await getMarketplaceCacheOnly(rootMarketplace)) |
| 395 | ?.allowCrossMarketplaceDependenciesOn |
| 396 | : undefined) ?? [], |
| 397 | ) |
| 398 | const resolution = await resolveDependencyClosure( |
| 399 | pluginId, |
| 400 | async id => { |
| 401 | if (depInfo.has(id)) return depInfo.get(id)!.entry |
| 402 | if (id === pluginId) return entry |
| 403 | const info = await getPluginById(id) |
| 404 | if (info) depInfo.set(id, info) |
| 405 | return info?.entry ?? null |
no test coverage detected