| import path from 'node:path'; |
|
|
| import { Error2, ErrorCodes } from '#/errors'; |
|
|
| export interface GithubRef { |
| readonly kind: 'branch' | 'tag' | 'sha'; |
| readonly value: string; |
| } |
|
|
| export type ResolvedSource = |
| | { kind: 'local-path'; path: string } |
| | { kind: 'zip-url'; path: string } |
| | { kind: 'github'; owner: string; repo: string; ref?: GithubRef }; |
|
|
| export type InstallSource = ResolvedSource; |
|
|
| const SHA_RE = /^[0-9a-f]{7,40}$/; |
|
|
| export function resolveInstallSource(source: string): ResolvedSource { |
| const trimmed = source.trim(); |
|
|
| const github = parseGithubUrl(trimmed); |
| if (github !== undefined) return github; |
|
|
| if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) { |
| return { kind: 'zip-url', path: trimmed }; |
| } |
| if (!path.isAbsolute(trimmed)) { |
| throw new Error2( |
| ErrorCodes.VALIDATION_FAILED, |
| `Plugin root must be an absolute path (got "${source}")`, |
| { details: { source } }, |
| ); |
| } |
| return { kind: 'local-path', path: trimmed }; |
| } |
|
|
| function parseGithubUrl(raw: string): ResolvedSource | undefined { |
| let url: URL; |
| try { |
| url = new URL(raw); |
| } catch { |
| return undefined; |
| } |
| if (url.protocol !== 'https:') return undefined; |
| if (url.hostname !== 'github.com' && url.hostname !== 'www.github.com') return undefined; |
|
|
| const segments = url.pathname.split('/').filter((s) => s.length > 0); |
| const owner = segments[0]; |
| const repoRaw = segments[1]; |
| if (owner === undefined || repoRaw === undefined) return undefined; |
|
|
| const repo = repoRaw.endsWith('.git') ? repoRaw.slice(0, -4) : repoRaw; |
| const rest = segments.slice(2); |
|
|
| if (rest.length === 0) { |
| return { kind: 'github', owner, repo }; |
| } |
|
|
| const head = rest[0]; |
| const second = rest[1]; |
|
|
| if (head === 'tree' && rest.length >= 2) { |
| const refValue = decodeRefSegments(rest.slice(1)); |
| const kind: GithubRef['kind'] = SHA_RE.test(refValue) ? 'sha' : 'branch'; |
| return { kind: 'github', owner, repo, ref: { kind, value: refValue } }; |
| } |
|
|
| if (head === 'releases' && second === 'tag' && rest.length >= 3) { |
| const tag = decodeRefSegments(rest.slice(2)); |
| return { kind: 'github', owner, repo, ref: { kind: 'tag', value: tag } }; |
| } |
|
|
| if (head === 'commit' && rest.length >= 2) { |
| const sha = decodeRefSegments(rest.slice(1)); |
| return { kind: 'github', owner, repo, ref: { kind: 'sha', value: sha } }; |
| } |
|
|
| return undefined; |
| } |
|
|
| function decodeRefSegments(segments: readonly string[]): string { |
| return segments |
| .map((segment) => { |
| try { |
| return decodeURIComponent(segment); |
| } catch { |
| return segment; |
| } |
| }) |
| .join('/'); |
| } |
|
|