( sourcePath: string, pluginId: string, version: string, entry?: PluginMarketplaceEntry, marketplaceDir?: string, )
| 363 | * @throws Error if the destination directory is empty after copy |
| 364 | */ |
| 365 | export async function copyPluginToVersionedCache( |
| 366 | sourcePath: string, |
| 367 | pluginId: string, |
| 368 | version: string, |
| 369 | entry?: PluginMarketplaceEntry, |
| 370 | marketplaceDir?: string, |
| 371 | ): Promise<string> { |
| 372 | // When zip cache is enabled, the canonical format is a ZIP file |
| 373 | const zipCacheMode = isPluginZipCacheEnabled() |
| 374 | const cachePath = getVersionedCachePath(pluginId, version) |
| 375 | const zipPath = getVersionedZipCachePath(pluginId, version) |
| 376 | |
| 377 | // If cache already exists (directory or ZIP), return it |
| 378 | if (zipCacheMode) { |
| 379 | if (await pathExists(zipPath)) { |
| 380 | logForDebugging( |
| 381 | `Plugin ${pluginId} version ${version} already cached at ${zipPath}`, |
| 382 | ) |
| 383 | return zipPath |
| 384 | } |
| 385 | } else if (await pathExists(cachePath)) { |
| 386 | const entries = await readdir(cachePath) |
| 387 | if (entries.length > 0) { |
| 388 | logForDebugging( |
| 389 | `Plugin ${pluginId} version ${version} already cached at ${cachePath}`, |
| 390 | ) |
| 391 | return cachePath |
| 392 | } |
| 393 | // Directory exists but is empty, remove it so we can recreate with content |
| 394 | logForDebugging( |
| 395 | `Removing empty cache directory for ${pluginId} at ${cachePath}`, |
| 396 | ) |
| 397 | await rmdir(cachePath) |
| 398 | } |
| 399 | |
| 400 | // Seed cache hit — return seed path in place (read-only, no copy). |
| 401 | // Callers handle both directory and .zip paths; this returns a directory. |
| 402 | const seedPath = await probeSeedCache(pluginId, version) |
| 403 | if (seedPath) { |
| 404 | logForDebugging( |
| 405 | `Using seed cache for ${pluginId}@${version} at ${seedPath}`, |
| 406 | ) |
| 407 | return seedPath |
| 408 | } |
| 409 | |
| 410 | // Create parent directories |
| 411 | await getFsImplementation().mkdir(dirname(cachePath)) |
| 412 | |
| 413 | // For local plugins: copy entry.source directory (the single source of truth) |
| 414 | // For remote plugins: marketplaceDir is undefined, fall back to copying sourcePath |
| 415 | if (entry && typeof entry.source === 'string' && marketplaceDir) { |
| 416 | const sourceDir = validatePathWithinBase(marketplaceDir, entry.source) |
| 417 | |
| 418 | logForDebugging( |
| 419 | `Copying source directory ${entry.source} for plugin ${pluginId}`, |
| 420 | ) |
| 421 | try { |
| 422 | await copyDir(sourceDir, cachePath) |
no test coverage detected