import { execSync } from 'child_process'; import { platform, homedir } from 'os'; import { join } from 'path'; import { existsSync, readFileSync } from 'fs'; /** * Intelligent Version Detection for Antigravity * * Detects versions from the local Antigravity installation's product.json. * Two version values are tracked: * - "version" field → X-Client-Version header (API version gate) * - "ideVersion" field → User-Agent version string * * Detection priority (for each): * 1. Environment variable override * 2. product.json from local Antigravity app * 3. OS-specific detection (macOS plist / Windows exe) * 4. Hardcoded fallback */ // Fallback for User-Agent version (ideVersion in product.json) const FALLBACK_USER_AGENT_VERSION = process.env.FALLBACK_ANTIGRAVITY_VERSION || '2.0.3'; // Fallback for X-Client-Version (top-level "version" in product.json) // Can be overridden via ANTIGRAVITY_CLIENT_VERSION_FALLBACK env var const FALLBACK_CLIENT_VERSION = process.env.ANTIGRAVITY_CLIENT_VERSION_FALLBACK || '1.110.0'; let cachedUserAgent = null; let cachedClientVersion = null; let cachedProductJson = undefined; // undefined = not yet attempted let loggedVersionInfo = false; /** * Compares two semver-ish version strings (X.Y.Z). * @returns {boolean} True if v1 > v2 */ function isVersionHigher(v1, v2) { const parts1 = v1.split('.').map(Number); const parts2 = v2.split('.').map(Number); for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { const p1 = parts1[i] || 0; const p2 = parts2[i] || 0; if (p1 > p2) return true; if (p1 < p2) return false; } return false; } /** * Returns platform-specific search paths for product.json. */ function getProductJsonPaths() { const os = platform(); const paths = []; if (os === 'darwin') { paths.push('/Applications/Antigravity.app/Contents/Resources/app/product.json'); paths.push(join(homedir(), 'Applications', 'Antigravity.app', 'Contents', 'Resources', 'app', 'product.json')); } else if (os === 'win32') { const localAppData = process.env.LOCALAPPDATA; const programFiles = process.env.ProgramFiles || 'C:\\Program Files'; if (localAppData) { paths.push(join(localAppData, 'Programs', 'Antigravity', 'resources', 'app', 'product.json')); } paths.push(join(programFiles, 'Antigravity', 'resources', 'app', 'product.json')); } else { paths.push('/usr/share/antigravity/resources/app/product.json'); paths.push('/opt/antigravity/resources/app/product.json'); paths.push('/opt/Antigravity/resources/app/product.json'); paths.push(join(homedir(), '.local', 'share', 'antigravity', 'resources', 'app', 'product.json')); paths.push('/snap/antigravity/current/resources/app/product.json'); } return paths; } /** * Find and parse product.json from the local Antigravity installation. * Caches the result after first attempt. * @returns {Object|null} Parsed product.json or null */ function getProductJson() { if (cachedProductJson !== undefined) return cachedProductJson; for (const p of getProductJsonPaths()) { try { if (existsSync(p)) { const content = JSON.parse(readFileSync(p, 'utf8')); if (content && (content.version || content.ideVersion)) { cachedProductJson = content; return content; } } } catch (e) { // Continue to next path } } cachedProductJson = null; return null; } /** * Log version detection results once at startup (lazy import to avoid circular deps). */ function logVersionInfo(version, source) { if (loggedVersionInfo) return; loggedVersionInfo = true; import('./logger.js').then(({ logger }) => { if (source === 'fallback') { logger.warn(`X-Client-Version: using hardcoded fallback ${version} — product.json not found. Set ANTIGRAVITY_CLIENT_VERSION (exact version) or ANTIGRAVITY_CLIENT_VERSION_FALLBACK (fallback value) env var to override.`); } else { logger.debug(`X-Client-Version: ${version} (source: ${source})`); } }).catch(() => {}); } /** * Get the X-Client-Version value for API requests. * Priority: ANTIGRAVITY_CLIENT_VERSION env var > product.json "version" > hardcoded fallback * @returns {string} Version string (e.g. "1.110.0") */ export function getClientVersion() { if (cachedClientVersion) return cachedClientVersion; if (process.env.ANTIGRAVITY_CLIENT_VERSION) { cachedClientVersion = process.env.ANTIGRAVITY_CLIENT_VERSION; logVersionInfo(cachedClientVersion, 'env'); return cachedClientVersion; } const product = getProductJson(); if (product?.version) { cachedClientVersion = product.version; logVersionInfo(cachedClientVersion, 'product.json'); return cachedClientVersion; } cachedClientVersion = FALLBACK_CLIENT_VERSION; logVersionInfo(cachedClientVersion, 'fallback'); return cachedClientVersion; } /** * Get the User-Agent version string. * Priority: FALLBACK_ANTIGRAVITY_VERSION env var > product.json "ideVersion" > OS detection > fallback * @returns {{ version: string, source: string }} */ function getUserAgentVersionConfig() { if (process.env.FALLBACK_ANTIGRAVITY_VERSION) { return { version: process.env.FALLBACK_ANTIGRAVITY_VERSION, source: 'env' }; } const product = getProductJson(); if (product?.ideVersion && isVersionHigher(product.ideVersion, FALLBACK_USER_AGENT_VERSION)) { return { version: product.ideVersion, source: 'product.json' }; } // OS-specific detection (legacy — reads app binary metadata directly) const os = platform(); let detectedVersion = null; try { if (os === 'darwin') { detectedVersion = getVersionMacos(); } else if (os === 'win32') { detectedVersion = getVersionWindows(); } } catch (error) { // Silently fail and use fallback } if (detectedVersion && isVersionHigher(detectedVersion, FALLBACK_USER_AGENT_VERSION)) { return { version: detectedVersion, source: 'local' }; } return { version: FALLBACK_USER_AGENT_VERSION, source: 'fallback' }; } /** * Generate a simplified User-Agent string used by the Antigravity binary. * Format: "antigravity/version os/arch" * @returns {string} The User-Agent string */ export function generateSmartUserAgent() { if (cachedUserAgent) return cachedUserAgent; const { version } = getUserAgentVersionConfig(); const os = platform(); const architecture = process.arch; const osName = os === 'darwin' ? 'darwin' : (os === 'win32' ? 'win32' : 'linux'); cachedUserAgent = `antigravity/${version} ${osName}/${architecture}`; return cachedUserAgent; } /** * MacOS-specific version detection using plutil */ function getVersionMacos() { const appPath = '/Applications/Antigravity.app'; const plistPath = join(appPath, 'Contents/Info.plist'); if (!existsSync(plistPath)) return null; try { const version = execSync(`plutil -extract CFBundleShortVersionString raw "${plistPath}"`, { encoding: 'utf8' }).trim(); if (/^\d+\.\d+\.\d+/.test(version)) { return version; } } catch (e) { // plutil failed or file not found } return null; } /** * Windows-specific version detection using PowerShell */ function getVersionWindows() { try { const localAppData = process.env.LOCALAPPDATA; const programFiles = process.env.ProgramFiles || 'C:\\Program Files'; const possiblePaths = [ join(localAppData, 'Programs', 'Antigravity', 'Antigravity.exe'), join(programFiles, 'Antigravity', 'Antigravity.exe') ]; for (const exePath of possiblePaths) { if (existsSync(exePath)) { const cmd = `powershell -Command "(Get-Item '${exePath}').VersionInfo.FileVersion"`; const version = execSync(cmd, { encoding: 'utf8' }).trim(); const match = version.match(/^(\d+\.\d+\.\d+)/); if (match) return match[1]; } } } catch (e) { // PowerShell or path issues } return null; }