( input: string, )
| 21 | * @returns MarketplaceSource object, error object, or null if format is unrecognized |
| 22 | */ |
| 23 | export async function parseMarketplaceInput( |
| 24 | input: string, |
| 25 | ): Promise<MarketplaceSource | { error: string } | null> { |
| 26 | const trimmed = input.trim() |
| 27 | const fs = getFsImplementation() |
| 28 | |
| 29 | // Handle git SSH URLs with any valid username (not just 'git') |
| 30 | // Supports: user@host:path, user@host:path.git, and with #ref suffix |
| 31 | // Username can contain: alphanumeric, dots, underscores, hyphens |
| 32 | const sshMatch = trimmed.match( |
| 33 | /^([a-zA-Z0-9._-]+@[^:]+:.+?(?:\.git)?)(#(.+))?$/, |
| 34 | ) |
| 35 | if (sshMatch?.[1]) { |
| 36 | const url = sshMatch[1] |
| 37 | const ref = sshMatch[3] |
| 38 | return ref ? { source: 'git', url, ref } : { source: 'git', url } |
| 39 | } |
| 40 | |
| 41 | // Handle URLs |
| 42 | if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) { |
| 43 | // Extract fragment (ref) from URL if present |
| 44 | const fragmentMatch = trimmed.match(/^([^#]+)(#(.+))?$/) |
| 45 | const urlWithoutFragment = fragmentMatch?.[1] || trimmed |
| 46 | const ref = fragmentMatch?.[3] |
| 47 | |
| 48 | // When user explicitly provides an HTTPS/HTTP URL that looks like a git |
| 49 | // repo, use the git source type so we clone rather than fetch-as-JSON. |
| 50 | // The .git suffix is a GitHub/GitLab/Bitbucket convention. Azure DevOps |
| 51 | // uses /_git/ in the path with NO suffix (appending .git breaks ADO: |
| 52 | // TF401019 "repo does not exist"). Without this check, an ADO URL falls |
| 53 | // through to source:'url' below, which tries to fetch it as a raw |
| 54 | // marketplace.json — the HTML response parses as "expected object, |
| 55 | // received string". (gh-31256 / CC-299) |
| 56 | if ( |
| 57 | urlWithoutFragment.endsWith('.git') || |
| 58 | urlWithoutFragment.includes('/_git/') |
| 59 | ) { |
| 60 | return ref |
| 61 | ? { source: 'git', url: urlWithoutFragment, ref } |
| 62 | : { source: 'git', url: urlWithoutFragment } |
| 63 | } |
| 64 | // Parse URL to check hostname |
| 65 | let url: URL |
| 66 | try { |
| 67 | url = new URL(urlWithoutFragment) |
| 68 | } catch (_err) { |
| 69 | // Not a valid URL for parsing, treat as generic URL |
| 70 | // new URL() throws TypeError for invalid URLs |
| 71 | return { source: 'url', url: urlWithoutFragment } |
| 72 | } |
| 73 | |
| 74 | if (url.hostname === 'github.com' || url.hostname === 'www.github.com') { |
| 75 | const match = url.pathname.match(/^\/([^/]+\/[^/]+?)(\/|\.git|$)/) |
| 76 | if (match?.[1]) { |
| 77 | // User explicitly provided HTTPS URL - keep it as HTTPS via 'git' type |
| 78 | // Add .git suffix if not present for proper git clone |
| 79 | const gitUrl = urlWithoutFragment.endsWith('.git') |
| 80 | ? urlWithoutFragment |
no test coverage detected