#!/usr/bin/env node /** * Antigravity "Always Proceed" Auto-Run Fix * ========================================== * * Fixes a bug where the "Always Proceed" terminal execution policy doesn't * actually auto-execute commands. Uses regex patterns to find code structures * regardless of minified variable names — works across versions. * * Usage: * node patch.js - Apply patch * node patch.js --revert - Restore original files * node patch.js --check - Check patch status * * License: MIT */ const fs = require('fs'); const path = require('path'); const os = require('os'); // ─── Installation Detection ───────────────────────────────────────────────── /** * Validates that a candidate directory is a real Antigravity installation * by checking for the workbench main JS file. */ function isAntigravityDir(dir) { if (!dir) return false; try { const workbench = path.join(dir, 'resources', 'app', 'out', 'vs', 'workbench', 'workbench.desktop.main.js'); return fs.existsSync(workbench); } catch { return false; } } /** * Checks if a directory looks like the Antigravity installation root * (contains Antigravity.exe or antigravity binary). */ function looksLikeAntigravityRoot(dir) { if (!dir) return false; try { const exe = process.platform === 'win32' ? 'Antigravity.exe' : 'antigravity'; return fs.existsSync(path.join(dir, exe)); } catch { return false; } } /** * Tries to find Antigravity installation path from Windows Registry. * InnoSetup writes uninstall info to HKCU or HKLM. */ function findFromRegistry() { if (process.platform !== 'win32') return null; try { const { execSync } = require('child_process'); // InnoSetup typically writes to this key; try HKCU first, then HKLM const regPaths = [ 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Antigravity_is1', 'HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Antigravity_is1', 'HKLM\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Antigravity_is1', ]; for (const regPath of regPaths) { try { const output = execSync( `reg query "${regPath}" /v InstallLocation`, { encoding: 'utf8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] } ); const match = output.match(/InstallLocation\s+REG_SZ\s+(.+)/i); if (match) { const dir = match[1].trim().replace(/\\$/, ''); if (isAntigravityDir(dir)) return dir; } } catch { /* key not found, try next */ } } } catch { /* child_process failed */ } return null; } /** * Tries to find Antigravity by looking at PATH entries for the executable. */ function findFromPath() { try { const pathDirs = (process.env.PATH || '').split(path.delimiter); const exe = process.platform === 'win32' ? 'Antigravity.exe' : 'antigravity'; for (const dir of pathDirs) { if (!dir) continue; if (fs.existsSync(path.join(dir, exe))) { // The exe could be in the root or in a bin/ subdirectory if (isAntigravityDir(dir)) return dir; const parent = path.dirname(dir); if (isAntigravityDir(parent)) return parent; } } } catch { /* PATH parsing failed */ } return null; } function findAntigravityPath() { // 1. Check CWD and its ancestors (user may run from install dir or a subdir) let dir = process.cwd(); const root = path.parse(dir).root; while (dir && dir !== root) { if (looksLikeAntigravityRoot(dir) && isAntigravityDir(dir)) return dir; dir = path.dirname(dir); } // 2. Check PATH const fromPath = findFromPath(); if (fromPath) return fromPath; // 3. Check Windows Registry (InnoSetup uninstall keys) const fromReg = findFromRegistry(); if (fromReg) return fromReg; // 4. Hardcoded well-known locations const candidates = []; if (process.platform === 'win32') { candidates.push( path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Antigravity'), path.join(process.env.PROGRAMFILES || '', 'Antigravity'), ); } else if (process.platform === 'darwin') { candidates.push( '/Applications/Antigravity.app/Contents/Resources', path.join(os.homedir(), 'Applications', 'Antigravity.app', 'Contents', 'Resources') ); } else { candidates.push('/usr/share/antigravity', '/opt/antigravity', path.join(os.homedir(), '.local', 'share', 'antigravity')); } for (const c of candidates) { if (isAntigravityDir(c)) return c; } return null; } // ─── Smart Pattern Matching ───────────────────────────────────────────────── /** * Finds the onChange handler for terminalAutoExecutionPolicy and extracts * variable names from context, regardless of minification. * * Pattern we're looking for (structure, not exact names): * =(()=>{ * ?.setTerminalAutoExecutionPolicy?.(), * ===.EAGER&&(!0) * },[...]) * * From the surrounding context we also extract: * = ?.terminalAutoExecutionPolicy ?? .OFF * = ?.secureModeEnabled ?? !1 */ function analyzeFile(content, label) { // 1. Find the onChange handler: contains setTerminalAutoExecutionPolicy AND .EAGER // Pattern: VARNAME=CALLBACK(ARG=>{...setTerminalAutoExecutionPolicy...,ARG===ENUM.EAGER&&CONFIRM(!0)},[...]) const onChangeRe = /(\w+)=(\w+)\((\w+)=>\{\w+\?\.setTerminalAutoExecutionPolicy\?\.\(\3\),\3===(\w+)\.EAGER&&(\w+)\(!0\)\},\[[\w,]*\]\)/; const onChangeMatch = content.match(onChangeRe); if (!onChangeMatch) { console.log(` ❌ [${label}] Could not find onChange handler pattern`); return null; } const [fullMatch, assignVar, callbackAlias, argName, enumAlias, confirmFn] = onChangeMatch; const matchIndex = content.indexOf(fullMatch); console.log(` 📋 [${label}] Found onChange at offset ${matchIndex}`); console.log(` callback=${callbackAlias}, enum=${enumAlias}, confirm=${confirmFn}`); // 2. Find policy variable: VARNAME=HANDLER?.terminalAutoExecutionPolicy??ENUM.OFF const policyRe = new RegExp(`(\\w+)=\\w+\\?\\.terminalAutoExecutionPolicy\\?\\?${enumAlias}\\.OFF`); const policyMatch = content.substring(Math.max(0, matchIndex - 2000), matchIndex).match(policyRe); if (!policyMatch) { console.log(` ❌ [${label}] Could not find policy variable`); return null; } const policyVar = policyMatch[1]; console.log(` policyVar=${policyVar}`); // 3. Find secureMode variable: VARNAME=HANDLER?.secureModeEnabled??!1 const secureRe = /(\w+)=\w+\?\.secureModeEnabled\?\?!1/; const secureMatch = content.substring(Math.max(0, matchIndex - 2000), matchIndex).match(secureRe); if (!secureMatch) { console.log(` ❌ [${label}] Could not find secureMode variable`); return null; } const secureVar = secureMatch[1]; console.log(` secureVar=${secureVar}`); // 4. Find useEffect alias: look for ALIAS(()=>{...},[...]) calls nearby (not useCallback/useMemo) const nearbyCode = content.substring(Math.max(0, matchIndex - 5000), matchIndex + 5000); const effectCandidates = {}; const effectRe = /\b(\w{2,3})\(\(\)=>\{[^}]{3,80}\},\[/g; let m; while ((m = effectRe.exec(nearbyCode)) !== null) { const alias = m[1]; if (alias !== callbackAlias && alias !== 'var' && alias !== 'new') { effectCandidates[alias] = (effectCandidates[alias] || 0) + 1; } } // Also check broader file for common useEffect patterns (with cleanup return) const cleanupRe = /\b(\w{2,3})\(\(\)=>\{[^}]*return\s*\(\)=>/g; while ((m = cleanupRe.exec(content)) !== null) { const alias = m[1]; if (alias !== callbackAlias) { effectCandidates[alias] = (effectCandidates[alias] || 0) + 5; // higher weight } } // Remove known non-useEffect aliases (useMemo patterns) // useMemo: alias(()=>EXPRESSION,[deps]) — returns a value, often assigned // useEffect: alias(()=>{STATEMENTS},[deps]) — no return value // Pick the most common candidate let useEffectAlias = null; let maxCount = 0; for (const [alias, count] of Object.entries(effectCandidates)) { if (count > maxCount) { maxCount = count; useEffectAlias = alias; } } if (!useEffectAlias) { console.log(` ❌ [${label}] Could not determine useEffect alias`); return null; } console.log(` useEffect=${useEffectAlias} (confidence: ${maxCount} hits)`); // 5. Build patch const patchCode = `_aep=${useEffectAlias}(()=>{${policyVar}===${enumAlias}.EAGER&&!${secureVar}&&${confirmFn}(!0)},[]),`; return { target: fullMatch, replacement: patchCode + fullMatch, patchMarker: `_aep=${useEffectAlias}(()=>{${policyVar}===${enumAlias}.EAGER`, label }; } // ─── File Operations ──────────────────────────────────────────────────────── function patchFile(filePath, label) { if (!fs.existsSync(filePath)) { console.log(` ❌ [${label}] File not found: ${filePath}`); return false; } const content = fs.readFileSync(filePath, 'utf8'); // Check if already patched if (content.includes('_aep=')) { const existingPatch = content.match(/_aep=\w+\(\(\)=>\{[^}]+EAGER[^}]+\},\[\]\)/); if (existingPatch) { console.log(` ⏭️ [${label}] Already patched`); return true; } } const analysis = analyzeFile(content, label); if (!analysis) return false; // Verify target is unique const count = content.split(analysis.target).length - 1; if (count !== 1) { console.log(` ❌ [${label}] Target found ${count} times (expected 1)`); return false; } // Backup if (!fs.existsSync(filePath + '.bak')) { fs.copyFileSync(filePath, filePath + '.bak'); console.log(` 📦 [${label}] Backup created`); } // Apply const patched = content.replace(analysis.target, analysis.replacement); fs.writeFileSync(filePath, patched, 'utf8'); const diff = fs.statSync(filePath).size - fs.statSync(filePath + '.bak').size; console.log(` ✅ [${label}] Patched (+${diff} bytes)`); return true; } function revertFile(filePath, label) { const bak = filePath + '.bak'; if (!fs.existsSync(bak)) { console.log(` ⏭️ [${label}] No backup, skipping`); return; } fs.copyFileSync(bak, filePath); console.log(` ✅ [${label}] Restored`); } function checkFile(filePath, label) { if (!fs.existsSync(filePath)) { console.log(` ❌ [${label}] Not found`); return false; } const content = fs.readFileSync(filePath, 'utf8'); const patched = content.includes('_aep=') && /_aep=\w+\(\(\)=>\{[^}]+EAGER/.test(content); const hasBak = fs.existsSync(filePath + '.bak'); if (patched) { console.log(` ✅ [${label}] PATCHED` + (hasBak ? ' (backup exists)' : '')); } else { const analysis = analyzeFile(content, label); if (analysis) { console.log(` ⬜ [${label}] NOT PATCHED (patchable)`); } else { console.log(` ⚠️ [${label}] NOT PATCHED (may be incompatible)`); } } return patched; } // ─── Version Info ─────────────────────────────────────────────────────────── function getVersion(basePath) { try { const pkg = JSON.parse(fs.readFileSync(path.join(basePath, 'resources', 'app', 'package.json'), 'utf8')); const product = JSON.parse(fs.readFileSync(path.join(basePath, 'resources', 'app', 'product.json'), 'utf8')); return `${pkg.version} (IDE ${product.ideVersion})`; } catch { return 'unknown'; } } // ─── Main ─────────────────────────────────────────────────────────────────── function main() { const args = process.argv.slice(2); const action = args.includes('--revert') ? 'revert' : args.includes('--check') ? 'check' : 'apply'; // Parse --path flag let explicitPath = null; const pathIdx = args.indexOf('--path'); if (pathIdx !== -1 && args[pathIdx + 1]) { explicitPath = path.resolve(args[pathIdx + 1]); } console.log(''); console.log('╔══════════════════════════════════════════════════╗'); console.log('║ Antigravity "Always Proceed" Auto-Run Fix ║'); console.log('╚══════════════════════════════════════════════════╝'); let basePath; if (explicitPath) { if (!isAntigravityDir(explicitPath)) { console.log(`\n\u274C --path "${explicitPath}" does not look like an Antigravity installation.`); console.log(' Expected to find: resources/app/out/vs/workbench/workbench.desktop.main.js'); process.exit(1); } basePath = explicitPath; } else { basePath = findAntigravityPath(); } if (!basePath) { console.log('\n\u274C Antigravity installation not found!'); console.log(''); console.log(' Try one of:'); console.log(' 1. Run from the Antigravity install directory:'); console.log(' cd "C:\\Path\\To\\Antigravity" && npx better-antigravity auto-run'); console.log(' 2. Specify the path explicitly:'); console.log(' npx better-antigravity auto-run --path "D:\\Antigravity"'); process.exit(1); } console.log(`\n📍 ${basePath}`); console.log(`📦 Version: ${getVersion(basePath)}`); console.log(''); const files = [ { path: path.join(basePath, 'resources', 'app', 'out', 'vs', 'workbench', 'workbench.desktop.main.js'), label: 'workbench' }, { path: path.join(basePath, 'resources', 'app', 'out', 'jetskiAgent', 'main.js'), label: 'jetskiAgent' }, ]; switch (action) { case 'check': files.forEach(f => checkFile(f.path, f.label)); break; case 'revert': files.forEach(f => revertFile(f.path, f.label)); console.log('\n✨ Restored! Restart Antigravity.'); break; case 'apply': const ok = files.every(f => patchFile(f.path, f.label)); console.log(ok ? '\n✨ Done! Restart Antigravity.\n💡 Run with --revert to undo.\n⚠️ Re-run after Antigravity updates.' : '\n⚠️ Some patches failed.'); break; } } main();