(repoUrl: string)
| 155 | } |
| 156 | |
| 157 | function deriveRepoFolderName(repoUrl: string): string { |
| 158 | const trimmedRepoUrl = repoUrl.trim(); |
| 159 | if (!trimmedRepoUrl) { |
| 160 | throw new Error("Repository URL cannot be empty"); |
| 161 | } |
| 162 | |
| 163 | let candidatePath = trimmedRepoUrl; |
| 164 | |
| 165 | // SSH-style shorthand: git@github.com:owner/repo.git |
| 166 | const scpLikeMatch = /^[^@\s]+@[^:\s]+:(.+)$/.exec(trimmedRepoUrl); |
| 167 | if (scpLikeMatch) { |
| 168 | candidatePath = scpLikeMatch[1]; |
| 169 | } else if (/^[^/\\\s]+\/[^/\\\s]+$/.test(trimmedRepoUrl)) { |
| 170 | // Owner/repo shorthand |
| 171 | candidatePath = trimmedRepoUrl; |
| 172 | } else { |
| 173 | try { |
| 174 | // https://..., ssh://..., file://... |
| 175 | const parsed = new URL(trimmedRepoUrl); |
| 176 | candidatePath = decodeURIComponent(parsed.pathname); |
| 177 | } catch { |
| 178 | // Not a URL with protocol. Treat as local path-like input. |
| 179 | } |
| 180 | } |
| 181 | |
| 182 | const normalizedCandidatePath = candidatePath.replace(/\\/g, "/").replace(/\/+$/, ""); |
| 183 | const repoName = path.posix.basename(normalizedCandidatePath).replace(/\.git$/i, ""); |
| 184 | const safeFolderName = sanitizeRepoFolderName(repoName); |
| 185 | |
| 186 | // Keep clone flow resilient even when the repo basename contains only symbols/emojis. |
| 187 | // A deterministic fallback avoids user-visible hard failures while staying shell-safe. |
| 188 | if (!safeFolderName) { |
| 189 | return deriveFallbackRepoFolderName(trimmedRepoUrl); |
| 190 | } |
| 191 | |
| 192 | return safeFolderName; |
| 193 | } |
| 194 | |
| 195 | const GITHUB_SHORTHAND_PATTERN = /^[a-zA-Z0-9][\w-]*\/[a-zA-Z0-9][\w.-]*$/; |
| 196 |
no test coverage detected