Files
gravity_control/antigravity-sdk-main/src/integration/workbench-patcher.ts
CD c3964f8e7a fix(bridge): rawRPC direct polling + SDK analysis docs + trial-and-error log
- Root cause: getDiagnostics.lastStepIndex is stale, SDK EventMonitor cannot detect real-time step changes
- Fix: Direct rawRPC('GetCascadeTrajectorySteps') polling every 5s
- Relay: PLANNER_RESPONSE, NOTIFY_USER, TASK_BOUNDARY, WAITING steps
- Added: docs/discord-bridge-analysis.md (full SDK architecture analysis)
- Added: docs/devlog/entries/20260308-003.md (trial-and-error history)
- Added: antigravity-sdk-main/ source reference
- Vikunja: #252 done, #253 created, #251 commented
2026-03-08 07:08:25 +09:00

258 lines
7.7 KiB
TypeScript

/**
* Workbench Patcher — Install/uninstall integration scripts into workbench.html.
*
* Handles the file-level modification of Antigravity's workbench.html
* to include/remove custom script tags.
*
* @module integration/workbench-patcher
*
* @internal
*/
import * as fs from 'fs';
import * as path from 'path';
/** Default prefix for generated files */
const FILE_PREFIX = 'ag-sdk';
/**
* Manages patching/unpatching of Antigravity's workbench.html.
*/
export class WorkbenchPatcher {
private readonly _workbenchDir: string;
private readonly _workbenchHtml: string;
private readonly _scriptPath: string;
private readonly _heartbeatPath: string;
private readonly _slug: string;
private readonly _markerStart: string;
private readonly _markerEnd: string;
/**
* @param namespace - Unique slug for this extension (e.g. 'kanezal-better-antigravity').
* Used to namespace all generated files and HTML markers so multiple
* SDK-based extensions can coexist without conflicts.
*/
constructor(namespace: string = 'default') {
// Resolve Antigravity install path
const appData = process.env.LOCALAPPDATA || '';
this._workbenchDir = path.join(
appData,
'Programs',
'Antigravity',
'resources',
'app',
'out',
'vs',
'code',
'electron-browser',
'workbench',
);
this._workbenchHtml = path.join(this._workbenchDir, 'workbench.html');
this._slug = namespace.replace(/[^a-zA-Z0-9-]/g, '-');
this._scriptPath = path.join(this._workbenchDir, `${FILE_PREFIX}-${this._slug}.js`);
this._heartbeatPath = path.join(this._workbenchDir, `${FILE_PREFIX}-${this._slug}-heartbeat`);
this._markerStart = `<!-- AG SDK [${this._slug}] -->`;
this._markerEnd = `<!-- /AG SDK [${this._slug}] -->`;
}
/**
* Check if workbench.html exists and is accessible.
*/
isAvailable(): boolean {
return fs.existsSync(this._workbenchHtml);
}
/**
* Check if our integration is currently installed.
*/
isInstalled(): boolean {
if (!this.isAvailable()) return false;
try {
const content = fs.readFileSync(this._workbenchHtml, 'utf8');
return content.includes(this._markerStart);
} catch {
return false;
}
}
/**
* Install the integration script.
*
* 1. Writes the script file to the workbench directory
* 2. Patches workbench.html to include a <script> tag
*
* @param scriptContent — The generated JavaScript code
*/
install(scriptContent: string): void {
if (!this.isAvailable()) {
throw new Error(`Workbench not found at: ${this._workbenchDir}`);
}
// First uninstall any previous integration for THIS namespace
if (this.isInstalled()) {
this.uninstall();
}
// Clean up legacy files from previous versions (non-namespaced)
this._cleanupLegacyFiles();
// Write the script file
fs.writeFileSync(this._scriptPath, scriptContent, 'utf8');
// Patch workbench.html
let html = fs.readFileSync(this._workbenchHtml, 'utf8');
// Insert before </html>
const scriptBasename = path.basename(this._scriptPath);
const scriptTag = [
this._markerStart,
`<script src="./${scriptBasename}"></script>`,
this._markerEnd,
].join('\n');
html = html.replace('</html>', `${scriptTag}\n</html>`);
fs.writeFileSync(this._workbenchHtml, html, 'utf8');
// Create empty titles JSON if it doesn't exist (prevents console 404)
const titlesPath = path.join(this._workbenchDir, `ag-sdk-titles-${this._slug}.json`);
if (!fs.existsSync(titlesPath)) {
fs.writeFileSync(titlesPath, '{}', 'utf8');
}
}
/**
* Remove the integration.
*
* 1. Removes the <script> tag from workbench.html
* 2. Deletes the script file
*/
uninstall(): void {
if (!this.isAvailable()) return;
// Remove from workbench.html
try {
let html = fs.readFileSync(this._workbenchHtml, 'utf8');
const regex = new RegExp(
`\\n?${escapeRegex(this._markerStart)}[\\s\\S]*?${escapeRegex(this._markerEnd)}\\n?`,
'g',
);
html = html.replace(regex, '');
fs.writeFileSync(this._workbenchHtml, html, 'utf8');
} catch {
// Ignore errors during cleanup
}
// Remove script file
try {
if (fs.existsSync(this._scriptPath)) {
fs.unlinkSync(this._scriptPath);
}
} catch {
// Ignore
}
}
/**
* Write/refresh the heartbeat marker file.
*
* The generated script checks this file's modification time
* to determine if the extension is still active. If the file
* is missing or stale, the script will not start.
*/
writeHeartbeat(): void {
try {
fs.writeFileSync(this._heartbeatPath, Date.now().toString(), 'utf8');
} catch {
// Ignore — workbench dir may not be writable
}
}
/**
* Remove the heartbeat marker file.
*/
removeHeartbeat(): void {
try {
if (fs.existsSync(this._heartbeatPath)) {
fs.unlinkSync(this._heartbeatPath);
}
} catch {
// Ignore
}
}
/**
* Get the path to the heartbeat file.
*/
getHeartbeatPath(): string {
return this._heartbeatPath;
}
/**
* Get the path to the workbench directory.
*/
getWorkbenchDir(): string {
return this._workbenchDir;
}
/**
* Get the path to the script file.
*/
getScriptPath(): string {
return this._scriptPath;
}
/**
* Clean up legacy files from previous SDK versions.
*
* Removes non-namespaced files (from before namespace support)
* and files with wrong namespace (e.g. 'undefined').
*/
private _cleanupLegacyFiles(): void {
// Legacy file names that may exist from older versions
const legacyFiles = [
'ag-sdk-integrate.js',
'ag-sdk-heartbeat',
'ag-sdk-titles.json',
'ag-sdk-titles-undefined.json',
'ag-sdk-titles-default.json',
];
for (const name of legacyFiles) {
const p = path.join(this._workbenchDir, name);
try {
if (fs.existsSync(p)) fs.unlinkSync(p);
} catch { /* ignore */ }
}
// Remove legacy script tags from workbench.html
try {
let html = fs.readFileSync(this._workbenchHtml, 'utf8');
let changed = false;
// Remove bare <script src="./ag-sdk-integrate.js"></script> lines
const legacyTagRegex = /<script src="\.\/ag-sdk-integrate\.js"><\/script>\n?/g;
if (legacyTagRegex.test(html)) {
html = html.replace(legacyTagRegex, '');
changed = true;
}
// Remove old X-Ray SDK markers with no namespace
const xrayRegex = /<!-- X-Ray SDK Integration -->\n?<script[^>]*ag-sdk-integrate[^>]*><\/script>\n?<!-- \/X-Ray SDK Integration -->\n?/g;
if (xrayRegex.test(html)) {
html = html.replace(xrayRegex, '');
changed = true;
}
if (changed) {
fs.writeFileSync(this._workbenchHtml, html, 'utf8');
}
} catch { /* ignore */ }
}
}
function escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}