( pluginId: string, entry: PluginMarketplaceEntry, scope: PluginScope = 'user', projectPath?: string, localSourcePath?: string, )
| 126 | * @returns The installation path |
| 127 | */ |
| 128 | export async function cacheAndRegisterPlugin( |
| 129 | pluginId: string, |
| 130 | entry: PluginMarketplaceEntry, |
| 131 | scope: PluginScope = 'user', |
| 132 | projectPath?: string, |
| 133 | localSourcePath?: string, |
| 134 | ): Promise<string> { |
| 135 | // For local plugins, we need the resolved absolute path |
| 136 | // Cast to PluginSource since cachePlugin handles any string path at runtime |
| 137 | const source: PluginSource = |
| 138 | typeof entry.source === 'string' && localSourcePath |
| 139 | ? (localSourcePath as PluginSource) |
| 140 | : entry.source |
| 141 | |
| 142 | const cacheResult = await cachePlugin(source, { |
| 143 | manifest: entry as PluginMarketplaceEntry, |
| 144 | }) |
| 145 | |
| 146 | // For local plugins, use the original source path for Git SHA calculation |
| 147 | // because the cached temp directory doesn't have .git (it's copied from a |
| 148 | // subdirectory of the marketplace git repo). For external plugins, use the |
| 149 | // cached path. For git-subdir sources, cachePlugin already captured the SHA |
| 150 | // before discarding the ephemeral clone (the extracted subdir has no .git). |
| 151 | const pathForGitSha = localSourcePath || cacheResult.path |
| 152 | const gitCommitSha = |
| 153 | cacheResult.gitCommitSha ?? (await getGitCommitSha(pathForGitSha)) |
| 154 | |
| 155 | const now = getCurrentTimestamp() |
| 156 | const version = await calculatePluginVersion( |
| 157 | pluginId, |
| 158 | entry.source, |
| 159 | cacheResult.manifest, |
| 160 | pathForGitSha, |
| 161 | entry.version, |
| 162 | cacheResult.gitCommitSha, |
| 163 | ) |
| 164 | |
| 165 | // Move the cached plugin to the versioned path: cache/marketplace/plugin/version/ |
| 166 | const versionedPath = getVersionedCachePath(pluginId, version) |
| 167 | let finalPath = cacheResult.path |
| 168 | |
| 169 | // Only move if the paths are different and plugin was cached to a different location |
| 170 | if (cacheResult.path !== versionedPath) { |
| 171 | // Create the versioned directory structure |
| 172 | await getFsImplementation().mkdir(dirname(versionedPath)) |
| 173 | |
| 174 | // Remove existing versioned path if present (force: no-op if missing) |
| 175 | await rm(versionedPath, { recursive: true, force: true }) |
| 176 | |
| 177 | // Check if versionedPath is a subdirectory of cacheResult.path |
| 178 | // This happens when marketplace name equals plugin name (e.g., "exa-mcp-server@exa-mcp-server") |
| 179 | // In this case, we can't directly rename because we'd be moving a directory into itself |
| 180 | const normalizedCachePath = cacheResult.path.endsWith(sep) |
| 181 | ? cacheResult.path |
| 182 | : cacheResult.path + sep |
| 183 | const isSubdirectory = versionedPath.startsWith(normalizedCachePath) |
| 184 | |
| 185 | if (isSubdirectory) { |
no test coverage detected