const ANTIGRAVITY_IDE_RELEASE_FEED_URL = "https://antigravity-auto-updater-974169037036.us-central1.run.app/releases"; const ANTIGRAVITY_CLI_RELEASE_URL = "https://api.github.com/repos/google-antigravity/antigravity-cli/releases/latest"; export const ANTIGRAVITY_VERSION_CACHE_TTL_MS = 6 * 60 * 60 * 1000; export const ANTIGRAVITY_VERSION_FETCH_TIMEOUT_MS = 5_000; export const ANTIGRAVITY_IDE_FALLBACK_VERSION = "2.1.1"; export const ANTIGRAVITY_CLI_FALLBACK_VERSION = "1.1.5"; type VersionCache = { fetchedAt: number; version: string; }; type ProductVersionState = { cache: VersionCache | null; inFlight: Promise | null; }; type FetchLike = typeof fetch; type VersionParser = (payload: unknown) => string | null; const ideState: ProductVersionState = { cache: null, inFlight: null }; const cliState: ProductVersionState = { cache: null, inFlight: null }; function normalizeVersion(value: unknown): string | null { if (typeof value !== "string") return null; const trimmed = value.trim().replace(/^v/i, ""); const match = trimmed.match(/^(\d+\.\d+\.\d+)\b/); return match ? match[1] : null; } function compareSemver(a: string, b: string): number { const aParts = a.split(".").map((part) => Number.parseInt(part, 10) || 0); const bParts = b.split(".").map((part) => Number.parseInt(part, 10) || 0); for (let i = 0; i < 3; i += 1) { if (aParts[i] !== bParts[i]) return aParts[i] - bParts[i]; } return 0; } function pickNewestVersion(...versions: unknown[]): string | null { return versions .map((version) => normalizeVersion(version)) .filter((version): version is string => !!version) .reduce( (best, version) => (!best || compareSemver(version, best) > 0 ? version : best), null ); } async function fetchJsonWithTimeout(fetchImpl: FetchLike, url: string): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), ANTIGRAVITY_VERSION_FETCH_TIMEOUT_MS); try { const response = await fetchImpl(url, { headers: { Accept: "application/json", "User-Agent": "OmniRoute-AntigravityVersion/1.0", }, signal: controller.signal, }); if (!response.ok) { throw new Error(`Version source ${url} returned ${response.status}`); } return response.json(); } finally { clearTimeout(timeoutId); } } function parseIdeReleaseFeed(payload: unknown): string | null { if (!Array.isArray(payload)) return null; return pickNewestVersion(...payload.map((entry) => (entry as { version?: unknown })?.version)); } function parseCliRelease(payload: unknown): string | null { if (!payload || typeof payload !== "object") return null; const release = payload as { name?: unknown; tag_name?: unknown }; return normalizeVersion(release.tag_name ?? release.name); } async function resolveProductVersion( state: ProductVersionState, fallbackVersion: string, sourceUrl: string, parsePayload: VersionParser, fetchImpl: FetchLike ): Promise { const now = Date.now(); if (state.cache && now - state.cache.fetchedAt < ANTIGRAVITY_VERSION_CACHE_TTL_MS) { return pickNewestVersion(state.cache.version, fallbackVersion) ?? fallbackVersion; } if (state.inFlight) { return state.inFlight; } state.inFlight = (async () => { let resolved: string | null = null; try { resolved = parsePayload(await fetchJsonWithTimeout(fetchImpl, sourceUrl)); } catch { resolved = null; } const version = pickNewestVersion(resolved, state.cache?.version, fallbackVersion) ?? fallbackVersion; if (resolved) { state.cache = { fetchedAt: Date.now(), version, }; } return version; })(); try { return await state.inFlight; } finally { state.inFlight = null; } } function seedVersionCache(state: ProductVersionState, version: string, fetchedAt: number): void { const normalized = normalizeVersion(version); if (!normalized) { throw new TypeError(`Invalid Antigravity version: ${version}`); } state.cache = { fetchedAt, version: normalized }; } export function resolveAntigravityIdeVersion(fetchImpl: FetchLike = fetch): Promise { return resolveProductVersion( ideState, ANTIGRAVITY_IDE_FALLBACK_VERSION, ANTIGRAVITY_IDE_RELEASE_FEED_URL, parseIdeReleaseFeed, fetchImpl ); } export function resolveAntigravityCliVersion(fetchImpl: FetchLike = fetch): Promise { return resolveProductVersion( cliState, ANTIGRAVITY_CLI_FALLBACK_VERSION, ANTIGRAVITY_CLI_RELEASE_URL, parseCliRelease, fetchImpl ); } export function getCachedAntigravityIdeVersion(): string { return ideState.cache?.version ?? ANTIGRAVITY_IDE_FALLBACK_VERSION; } export function getCachedAntigravityCliVersion(): string { return cliState.cache?.version ?? ANTIGRAVITY_CLI_FALLBACK_VERSION; } export function seedAntigravityIdeVersionCache(version: string, fetchedAt = Date.now()): void { seedVersionCache(ideState, version, fetchedAt); } export function seedAntigravityCliVersionCache(version: string, fetchedAt = Date.now()): void { seedVersionCache(cliState, version, fetchedAt); } export function clearAntigravityVersionCaches(): void { ideState.cache = null; ideState.inFlight = null; cliState.cache = null; cliState.inFlight = null; }