()
| 56 | * Results are cached for 30 seconds to avoid repeated filesystem scans |
| 57 | */ |
| 58 | export async function discoverInstalledPlugins(): Promise<PluginInfo[]> { |
| 59 | // Return cached result if still valid |
| 60 | if (pluginCache && Date.now() - pluginCache.timestamp < CACHE_TTL_MS) { |
| 61 | return pluginCache.plugins |
| 62 | } |
| 63 | |
| 64 | const plugins: PluginInfo[] = [] |
| 65 | const marketplacesDir = path.join(os.homedir(), ".claude", "plugins", "marketplaces") |
| 66 | |
| 67 | try { |
| 68 | await fs.access(marketplacesDir) |
| 69 | } catch { |
| 70 | pluginCache = { plugins, timestamp: Date.now() } |
| 71 | return plugins |
| 72 | } |
| 73 | |
| 74 | let marketplaces: Dirent[] |
| 75 | try { |
| 76 | marketplaces = await fs.readdir(marketplacesDir, { withFileTypes: true }) |
| 77 | } catch { |
| 78 | pluginCache = { plugins, timestamp: Date.now() } |
| 79 | return plugins |
| 80 | } |
| 81 | |
| 82 | for (const marketplace of marketplaces) { |
| 83 | if (marketplace.name.startsWith(".")) continue |
| 84 | |
| 85 | const isMarketplaceDir = await isDirentDirectory( |
| 86 | marketplacesDir, |
| 87 | marketplace, |
| 88 | ) |
| 89 | if (!isMarketplaceDir) continue |
| 90 | |
| 91 | const marketplacePath = path.join(marketplacesDir, marketplace.name) |
| 92 | const marketplaceJsonPath = path.join(marketplacePath, ".claude-plugin", "marketplace.json") |
| 93 | |
| 94 | try { |
| 95 | const content = await fs.readFile(marketplaceJsonPath, "utf-8") |
| 96 | |
| 97 | let marketplaceJson: MarketplaceJson |
| 98 | try { |
| 99 | marketplaceJson = JSON.parse(content) |
| 100 | } catch { |
| 101 | continue |
| 102 | } |
| 103 | |
| 104 | if (!Array.isArray(marketplaceJson.plugins)) { |
| 105 | continue |
| 106 | } |
| 107 | |
| 108 | for (const plugin of marketplaceJson.plugins) { |
| 109 | // Validate plugin.source exists |
| 110 | if (!plugin.source) continue |
| 111 | |
| 112 | // source can be a string path or an object { source: "url", url: "..." } |
| 113 | const sourcePath = typeof plugin.source === "string" ? plugin.source : null |
| 114 | if (!sourcePath) continue |
| 115 |
no test coverage detected