Files
gravity_control/better-antigravity-main/src/auto-run.ts
CD c97414cd37 fix(bridge): stall-based approval detection + known issues from deep debugging
- IDLE→stall detection: RUNNING+delta=0 for 6 polls (30s)
- lastModifiedTime-based thinking filter (partial)
- ResolveOutstandingSteps confirmed CANCELS steps (removed)
- HandleCascadeUserInteraction always socket hang up (removed)
- VS Code accept commands: silent success, no effect
- Hybrid approval: focus+all commands sequential, no break
- logToFile: console.log backup added
- Known issues: 4 critical findings documented
- better-antigravity reference added for future research
2026-03-08 14:38:41 +09:00

236 lines
7.2 KiB
TypeScript

/**
* 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<boolean> {
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: <callback>=<useCallback>((<arg>)=>{<setFn>(<arg>),<arg>===<ENUM>.EAGER&&<confirm>(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: <var>=<something>?.terminalAutoExecutionPolicy??<ENUM>.OFF
const policyMatch = /(\w+)=\w+\?\.terminalAutoExecutionPolicy\?\?(\w+)\.OFF/.exec(context);
// secureVar: <var>=<something>?.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<string, number> = {};
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<PatchResult> {
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<PatchResult[]> {
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));
}