/** * Auto-Run Fix — Patches the "Always Proceed" terminal policy to actually auto-execute. * * Uses structural regex matching to find the onChange handler in minified code * and injects a missing useEffect that auto-confirms commands when policy is EAGER. * * Works across AG versions because it matches code STRUCTURE, not variable NAMES. * * @module auto-run */ import * as path from 'path'; import * as fs from 'fs'; import * as fsp from 'fs/promises'; /** Marker comment to identify our patches */ const PATCH_MARKER = '/*BA:autorun*/'; /** * Resolve the Antigravity workbench directory. */ export function getWorkbenchDir(): string | null { const appData = process.env.LOCALAPPDATA || ''; const dir = path.join( appData, 'Programs', 'Antigravity', 'resources', 'app', 'out', 'vs', 'code', 'electron-browser', 'workbench', ); return fs.existsSync(dir) ? dir : null; } /** * Target files that need the auto-run patch. */ export function getTargetFiles(workbenchDir: string): Array<{ path: string; label: string }> { return [ { path: path.join(workbenchDir, 'workbench.desktop.main.js'), label: 'workbench' }, { path: path.join(workbenchDir, 'jetskiAgent.js'), label: 'jetskiAgent' }, ].filter(f => fs.existsSync(f.path)); } /** * Check if a file already has the auto-run patch applied. */ export async function isPatched(filePath: string): Promise { try { // Read only first 50 bytes of the marker area via a small buffer scan // The marker is injected mid-file, so we must read the full file. // Use async to avoid blocking extension host. const content = await fsp.readFile(filePath, 'utf8'); return content.includes(PATCH_MARKER); } catch { return false; } } /** * Analyze a file to find the onChange handler and extract variable names. * * Returns null if pattern not found (file may already be fixed by AG update). */ function analyzeFile(content: string): AnalysisResult | null { // Find onChange handler for terminalAutoExecutionPolicy // Pattern: =(()=>{(),===.EAGER&&(true)},[...]) const onChangeRegex = /(\w+)=(\w+)\((\(\w+\))=>\{(\w+)\(\w+\),\w+===(\w+)\.EAGER&&(\w+)\(!0\)\},\[/g; const match = onChangeRegex.exec(content); if (!match) return null; const [fullMatch, , , , , enumName, confirmFn] = match; const insertPos = match.index + fullMatch.length; // Extract context variables from surrounding code const contextStart = Math.max(0, match.index - 3000); const contextEnd = Math.min(content.length, match.index + 3000); const context = content.substring(contextStart, contextEnd); // policyVar: =?.terminalAutoExecutionPolicy??.OFF const policyMatch = /(\w+)=\w+\?\.terminalAutoExecutionPolicy\?\?(\w+)\.OFF/.exec(context); // secureVar: =?.secureModeEnabled??!1 const secureMatch = /(\w+)=\w+\?\.secureModeEnabled\?\?!1/.exec(context); if (!policyMatch || !secureMatch) return null; const policyVar = policyMatch[1]; const secureVar = secureMatch[1]; // Find useEffect — most frequently used short-named function in the scope const useEffectFn = findUseEffect(context, [confirmFn]); if (!useEffectFn) return null; // Find insertion point: after the useCallback closing const afterOnChange = content.indexOf('])', insertPos); if (afterOnChange === -1) return null; const insertAt = content.indexOf(';', afterOnChange); if (insertAt === -1) return null; return { enumName, confirmFn, policyVar, secureVar, useEffectFn, insertAt: insertAt + 1, }; } /** * Find the useEffect function name by frequency analysis. */ function findUseEffect(context: string, exclude: string[]): string | null { const candidates: Record = {}; const regex = /(\w{1,3})\(\(\)=>\{/g; let m; while ((m = regex.exec(context)) !== null) { const fn = m[1]; if (fn.length <= 3 && !exclude.includes(fn)) { candidates[fn] = (candidates[fn] || 0) + 1; } } let best = ''; let maxCount = 0; for (const [fn, count] of Object.entries(candidates)) { if (count > maxCount) { best = fn; maxCount = count; } } return best || null; } interface AnalysisResult { enumName: string; confirmFn: string; policyVar: string; secureVar: string; useEffectFn: string; insertAt: number; } /** * Apply the auto-run patch to a single file. * * @returns Patch status message */ export async function patchFile(filePath: string, label: string): Promise { try { let content = await fsp.readFile(filePath, 'utf8'); if (content.includes(PATCH_MARKER)) { return { success: true, label, status: 'already-patched' }; } const analysis = analyzeFile(content); if (!analysis) { return { success: false, label, status: 'pattern-not-found' }; } const { enumName, confirmFn, policyVar, secureVar, useEffectFn, insertAt } = analysis; // Build the patch const patch = `${PATCH_MARKER}${useEffectFn}(()=>{${policyVar}===${enumName}.EAGER&&!${secureVar}&&${confirmFn}(!0)},[])`; // Create backup (only if one doesn't exist) const backup = filePath + '.ba-backup'; try { await fsp.access(backup); } catch { await fsp.copyFile(filePath, backup); } // Insert content = content.substring(0, insertAt) + patch + content.substring(insertAt); await fsp.writeFile(filePath, content, 'utf8'); return { success: true, label, status: 'patched', bytesAdded: patch.length }; } catch (err: any) { return { success: false, label, status: 'error', error: err.message }; } } /** * Revert the auto-run patch on a single file. */ export function revertFile(filePath: string, label: string): PatchResult { const backup = filePath + '.ba-backup'; if (!fs.existsSync(backup)) { return { success: false, label, status: 'no-backup' }; } try { fs.copyFileSync(backup, filePath); fs.unlinkSync(backup); return { success: true, label, status: 'reverted' }; } catch (err: any) { return { success: false, label, status: 'error', error: err.message }; } } export interface PatchResult { success: boolean; label: string; status: 'patched' | 'already-patched' | 'pattern-not-found' | 'reverted' | 'no-backup' | 'error'; bytesAdded?: number; error?: string; } /** * Auto-apply the fix to all target files. * * @returns Array of results for each file */ export async function autoApply(): Promise { const dir = getWorkbenchDir(); if (!dir) return []; const files = getTargetFiles(dir); return Promise.all(files.map(f => patchFile(f.path, f.label))); } /** * Revert all target files from backups. * * @returns Number of files reverted */ export function revertAll(): PatchResult[] { const dir = getWorkbenchDir(); if (!dir) return []; const files = getTargetFiles(dir); return files.map(f => revertFile(f.path, f.label)); }