(
url: string,
options: {
repoMeta?: GitHubRepoMeta | null;
/**
* Caller-controlled rate-limit retry budget for the underlying GitHub API
* calls. Server actions should pass a small value (e.g. 3000) to stay
* under their function timeout; bulk scripts can pass 30000 (the default).
*/
maxWaitMs?: number;
} = {},
)
| 285 | } |
| 286 | |
| 287 | export async function parseGitHubPlugin( |
| 288 | url: string, |
| 289 | options: { |
| 290 | repoMeta?: GitHubRepoMeta | null; |
| 291 | /** |
| 292 | * Caller-controlled rate-limit retry budget for the underlying GitHub API |
| 293 | * calls. Server actions should pass a small value (e.g. 3000) to stay |
| 294 | * under their function timeout; bulk scripts can pass 30000 (the default). |
| 295 | */ |
| 296 | maxWaitMs?: number; |
| 297 | } = {}, |
| 298 | ): Promise<ParsedPlugin> { |
| 299 | const parsed = parseGitHubUrl(url); |
| 300 | if (!parsed) { |
| 301 | throw new GitHubParseError( |
| 302 | "Invalid GitHub URL. Expected format: https://github.com/owner/repo", |
| 303 | "invalid_url", |
| 304 | ); |
| 305 | } |
| 306 | |
| 307 | const { owner, repo } = parsed; |
| 308 | const fetchOpts: FetchOptions = { maxWaitMs: options.maxWaitMs }; |
| 309 | |
| 310 | const tree = await fetchGitHubTree(owner, repo, fetchOpts); |
| 311 | if (tree.length === 0) { |
| 312 | throw new GitHubParseError( |
| 313 | "Could not read repository. Make sure the repo exists, is public, and the URL is correct.", |
| 314 | "repo_unreadable", |
| 315 | ); |
| 316 | } |
| 317 | |
| 318 | const rootManifestPaths = [ |
| 319 | ".plugin/plugin.json", |
| 320 | ".cursor-plugin/plugin.json", |
| 321 | ".claude-plugin/plugin.json", |
| 322 | ".cursor-plugin/marketplace.json", |
| 323 | ]; |
| 324 | let manifest: Record<string, unknown> = {}; |
| 325 | for (const mp of rootManifestPaths) { |
| 326 | const content = await fetchGitHubFile(owner, repo, mp); |
| 327 | if (content) { |
| 328 | try { |
| 329 | manifest = JSON.parse(content); |
| 330 | } catch { |
| 331 | // ignore invalid JSON |
| 332 | } |
| 333 | break; |
| 334 | } |
| 335 | } |
| 336 | |
| 337 | // Discover sub-plugin dirs. Two patterns are supported: |
| 338 | // 1. Marketplace manifest lists `plugins[].source` (relative dirs). |
| 339 | // 2. Any `<dir>/.cursor-plugin/plugin.json` in the tree. |
| 340 | const subPluginDirs = new Set<string>(); |
| 341 | |
| 342 | if (Array.isArray(manifest.plugins)) { |
| 343 | for (const entry of manifest.plugins as Array<unknown>) { |
| 344 | if (entry && typeof entry === "object") { |
no test coverage detected