antigravity-proxy / src /utils /version-detector.js
badri-s's picture
chore: update models to reflect current upstream lineup
8695653
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;
}