// Rules for the editable project fields: name and GitHub repo links // (R-project-repo-links). Shared by the registry, the JSON API, and the CLI. // Keep this module free of package dependencies because the installed CLI // imports it directly. export const MAX_PROJECT_NAME_LENGTH = 80; export const PROJECT_NAME_ERROR = `Project names must contain 1 to ${MAX_PROJECT_NAME_LENGTH} characters.`; export const PROJECT_REPO_ERROR = "Repo links must look like https://github.com/owner/repo."; export const projectNameError = (name: unknown): string | undefined => { if (typeof name !== "string") return PROJECT_NAME_ERROR; const length = name.trim().length; return length === 0 || length > MAX_PROJECT_NAME_LENGTH ? PROJECT_NAME_ERROR : undefined; }; export type ProjectRepoParseResult = { error: string } | { url: string }; /** * Parse one GitHub repo link into the stored form * `https://github.com//`. */ export const parseProjectRepoUrl = (value: unknown): ProjectRepoParseResult => { if (typeof value !== "string") return { error: PROJECT_REPO_ERROR }; let parsed: URL; try { parsed = new URL(value.trim()); } catch { return { error: PROJECT_REPO_ERROR }; } if (parsed.protocol !== "https:") return { error: PROJECT_REPO_ERROR }; if (parsed.hostname.toLowerCase() !== "github.com") return { error: PROJECT_REPO_ERROR }; const segments = parsed.pathname.split("/").filter(Boolean); if (segments.length !== 2) return { error: PROJECT_REPO_ERROR }; const owner = segments[0]; const repo = segments[1].replace(/\.git$/i, ""); if (!/^[A-Za-z0-9._-]+$/.test(owner) || !/^[A-Za-z0-9._-]+$/.test(repo)) { return { error: PROJECT_REPO_ERROR }; } return { url: `https://github.com/${owner}/${repo}` }; };