export type GiteaReleaseAsset = { name?: unknown; browser_download_url?: unknown; size?: unknown; }; export type GiteaRelease = { tag_name?: unknown; name?: unknown; html_url?: unknown; assets?: unknown; }; export type InstallerAsset = { name: string; downloadUrl: string; size?: number; }; export type ReleaseUpdate = { version: string; title: string; pageUrl?: string; installer: InstallerAsset; }; export type UpdateFileConfig = { enabled?: unknown; apiUrl?: unknown; releasePageUrl?: unknown; assetPattern?: unknown; }; export function isVersionNewer(candidateVersion: string, currentVersion: string): boolean { const candidate = normalizeVersion(candidateVersion); const current = normalizeVersion(currentVersion); for (let index = 0; index < Math.max(candidate.length, current.length); index += 1) { const candidatePart = candidate[index] ?? 0; const currentPart = current[index] ?? 0; if (candidatePart > currentPart) { return true; } if (candidatePart < currentPart) { return false; } } return false; } export function getReleaseVersion(release: GiteaRelease): string | null { const tagName = typeof release.tag_name === "string" ? release.tag_name.trim() : ""; return tagName.length > 0 ? tagName.replace(/^v/i, "") : null; } export function getReleaseTitle(release: GiteaRelease, version: string): string { const releaseName = typeof release.name === "string" ? release.name.trim() : ""; return releaseName.length > 0 ? releaseName : `Scoreko ${version}`; } export function selectInstallerAsset(release: GiteaRelease, assetPattern: string): InstallerAsset | null { const assets = Array.isArray(release.assets) ? release.assets : []; const matcher = new RegExp(assetPattern, "i"); for (const asset of assets as GiteaReleaseAsset[]) { const name = typeof asset.name === "string" ? asset.name : ""; const downloadUrl = typeof asset.browser_download_url === "string" ? asset.browser_download_url : ""; if (!name || !downloadUrl || !matcher.test(name)) { continue; } return { name, downloadUrl, ...(typeof asset.size === "number" ? { size: asset.size } : {}), }; } return null; } export function buildReleaseUpdate( release: GiteaRelease, currentVersion: string, assetPattern: string, ): ReleaseUpdate | null { const version = getReleaseVersion(release); if (!version || !isVersionNewer(version, currentVersion)) { return null; } const installer = selectInstallerAsset(release, assetPattern); if (!installer) { return null; } const pageUrl = typeof release.html_url === "string" && release.html_url.length > 0 ? release.html_url : undefined; return { version, title: getReleaseTitle(release, version), pageUrl, installer, }; } export function sanitizeFileName(fileName: string): string { return fileName.replace(/[<>:"/\\|?*\x00-\x1f]/g, "_"); } function normalizeVersion(version: string): number[] { return version .trim() .replace(/^v/i, "") .split(/[+-]/)[0] .split(".") .map((part) => Number.parseInt(part, 10)) .map((part) => (Number.isFinite(part) ? part : 0)); }