Files
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

401 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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):
* <VAR_CONFIRM>=<useCallback>((<ARG>)=>{
* <stepHandler>?.setTerminalAutoExecutionPolicy?.(<ARG>),
* <ARG>===<ENUM>.EAGER&&<CONFIRM_FN>(!0)
* },[...])
*
* From the surrounding context we also extract:
* <POLICY_VAR> = <stepHandler>?.terminalAutoExecutionPolicy ?? <ENUM>.OFF
* <SECURE_VAR> = <stepHandler>?.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();