import semver from "semver"; import browser from "webextension-polyfill"; import { settingsState } from "@/seqta/utils/listeners/SettingsState"; const CHECK_THROTTLE_MS = 6 * 60 * 60 * 1000; const LAST_CHECK_KEY = "bsplus_lastGhReleaseCheck"; const NIGHTLY_TAG = "nightly"; let cachedResult: GhReleaseUpdateInfo | null = null; export interface GhReleaseUpdateInfo { available: boolean; label: string; url: string; } function isUpdateCheckEnabled(): boolean { return typeof __ENABLE_GH_RELEASE_UPDATE_CHECK__ !== "undefined" && __ENABLE_GH_RELEASE_UPDATE_CHECK__; } function getRepoSlug(): string { return typeof __GH_RELEASE_REPO__ !== "undefined" ? __GH_RELEASE_REPO__ : "BetterSEQTA/BetterSEQTA-Plus"; } function getUpdateChannel(): "stable" | "nightly" { return typeof __UPDATE_CHANNEL__ !== "undefined" ? __UPDATE_CHANNEL__ : "stable"; } function getBuildLabel(): string { return typeof __BUILD_LABEL__ !== "undefined" ? __BUILD_LABEL__ : ""; } function getCurrentVersion(): string { return browser.runtime.getManifest().version; } function releasesBaseUrl(): string { return `https://github.com/${getRepoSlug()}/releases`; } function shouldThrottleCheck(): boolean { try { const last = localStorage.getItem(LAST_CHECK_KEY); if (!last) return false; return Date.now() - Number(last) < CHECK_THROTTLE_MS; } catch { return false; } } function markChecked(): void { try { localStorage.setItem(LAST_CHECK_KEY, String(Date.now())); } catch { /* ignore */ } } function getDevOverrideVersion(): string | null { if (!settingsState.devMode) return null; const override = settingsState.devGhReleaseVersionOverride?.trim(); return override || null; } function compareWithOverride(current: string): GhReleaseUpdateInfo | null { const override = getDevOverrideVersion(); if (!override) return null; const currentCoerced = semver.coerce(current); const overrideCoerced = semver.coerce(override); if (!currentCoerced || !overrideCoerced) return null; if (semver.gt(overrideCoerced, currentCoerced)) { return { available: true, label: override, url: releasesBaseUrl(), }; } return { available: false, label: "", url: releasesBaseUrl() }; } interface GhRelease { tag_name: string; published_at: string; prerelease: boolean; } async function fetchJson(url: string): Promise { try { const response = await fetch(url, { headers: { Accept: "application/vnd.github+json" }, }); if (!response.ok) return null; return (await response.json()) as T; } catch { return null; } } function isStableSemverTag(tag: string): boolean { if (tag === NIGHTLY_TAG) return false; return semver.valid(semver.coerce(tag)) !== null; } async function checkStableUpdate(current: string): Promise { const url = releasesBaseUrl(); const releases = await fetchJson( `https://api.github.com/repos/${getRepoSlug()}/releases`, ); if (!releases?.length) { return { available: false, label: "", url }; } let latestTag: string | null = null; let latestVersion: semver.SemVer | null = null; for (const release of releases) { const tag = release.tag_name; if (!isStableSemverTag(tag)) continue; const coerced = semver.coerce(tag); if (!coerced) continue; if (!latestVersion || semver.gt(coerced, latestVersion)) { latestVersion = coerced; latestTag = tag; } } const currentCoerced = semver.coerce(current); if (!latestTag || !latestVersion || !currentCoerced) { return { available: false, label: "", url }; } if (semver.gt(latestVersion, currentCoerced)) { return { available: true, label: latestTag, url: `${url}/tag/${latestTag}` }; } return { available: false, label: "", url }; } async function checkNightlyUpdate(): Promise { const url = `${releasesBaseUrl()}/tag/${NIGHTLY_TAG}`; const release = await fetchJson( `https://api.github.com/repos/${getRepoSlug()}/releases/tags/${NIGHTLY_TAG}`, ); if (!release?.published_at) { return { available: false, label: "", url }; } const lastSeen = settingsState.lastSeenNightlyPublishedAt; const buildLabel = getBuildLabel(); const label = buildLabel ? `nightly #${buildLabel}` : "nightly"; if (!lastSeen) { settingsState.lastSeenNightlyPublishedAt = release.published_at; return { available: false, label: "", url }; } if (new Date(release.published_at) > new Date(lastSeen)) { return { available: true, label, url }; } return { available: false, label: "", url }; } export function isGhReleaseUpdateCheckEnabled(): boolean { return isUpdateCheckEnabled(); } export async function checkGithubReleaseUpdate(): Promise { const fallback = { available: false, label: "", url: releasesBaseUrl() }; if (!isUpdateCheckEnabled()) return fallback; const current = getCurrentVersion(); const overrideResult = compareWithOverride(current); if (overrideResult) return overrideResult; if (shouldThrottleCheck()) { return cachedResult ?? fallback; } markChecked(); const result = getUpdateChannel() === "nightly" ? await checkNightlyUpdate() : await checkStableUpdate(current); cachedResult = result; return result; } export function dismissNightlyUpdate(): void { void (async () => { const release = await fetchJson( `https://api.github.com/repos/${getRepoSlug()}/releases/tags/${NIGHTLY_TAG}`, ); if (release?.published_at) { settingsState.lastSeenNightlyPublishedAt = release.published_at; } })(); }