antigravity-rtl-patcher 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,77 +1,115 @@
1
1
  # 🌌 Antigravity RTL Patcher
2
2
 
3
3
  <div align="center">
4
- <img src="https://img.shields.io/npm/v/antigravity-rtl-patcher?color=cyan&label=Version&style=for-the-badge" alt="Version">
5
- <img src="https://img.shields.io/npm/dt/antigravity-rtl-patcher?color=magenta&style=for-the-badge" alt="Downloads">
6
- <img src="https://img.shields.io/badge/License-MIT-green.svg?style=for-the-badge" alt="License">
7
- </div>
8
4
 
9
- <br>
5
+ ![Version](https://img.shields.io/npm/v/antigravity-rtl-patcher?color=%2300bcd4&style=for-the-badge&label=VERSION)
6
+ ![Downloads](https://img.shields.io/npm/dt/antigravity-rtl-patcher?color=%23e040fb&style=for-the-badge)
7
+ ![License](https://img.shields.io/badge/LICENSE-MIT-brightgreen?style=for-the-badge)
8
+ ![Node](https://img.shields.io/badge/NODE-%3E%3D16-339933?style=for-the-badge&logo=node.js&logoColor=white)
9
+
10
+ **Intelligent RTL & Persian Typography Engine for Antigravity IDE**
11
+
12
+ *One command. Full right-to-left support. Zero config.*
10
13
 
11
- <div align="center">
12
- <h3>An elegant, intelligent, and seamless RTL & Typography patcher for Antigravity IDE.</h3>
13
- <p>Bring native support for Persian, Arabic, and Hebrew to your favorite AI IDE.</p>
14
14
  </div>
15
15
 
16
16
  ---
17
17
 
18
- ## ⚔ What is this?
19
- Antigravity IDE (like many modern AI editors) is built on Electron. Out of the box, it lacks proper Right-to-Left (RTL) support. When AI generates Persian or Arabic text, the alignment breaks, fonts look terrible, and mixed English/RTL sentences become unreadable.
18
+ ## 🧬 How It Works
20
19
 
21
- This tool is a **CLI Patcher**. It deeply hooks into the core of Antigravity IDE (`app.asar`), injects a smart JavaScript `MutationObserver` engine, and automatically formats text direction dynamically.
20
+ Antigravity IDE is built on Electron. This CLI tool directly patches the IDE's core workbench files to inject a smart **MutationObserver** engine that:
22
21
 
23
- ## šŸš€ Features
24
- - **🧠 Smart Direction:** Detects RTL languages using Regex and applies alignment only where necessary.
25
- - **šŸŽØ Beautiful Typography:** Automatically injects the modern **Vazirmatn** font for perfect legibility.
26
- - **🧩 Mixed Text Support:** Uses `unicode-bidi: plaintext` to ensure English code blocks inside Persian text don't break.
27
- - **šŸ›”ļø Safe & Reversible:** Automatically creates a backup. You can restore the IDE to its original state with a single command.
28
- - **āš™ļø OS Agnostic:** Works on Windows, macOS, and Linux out of the box.
22
+ 1. šŸ” **Watches** the DOM for new chat messages and AI responses in real-time
23
+ 2. 🧠 **Detects** Persian/Arabic/Hebrew text using Unicode analysis with ratio-based scoring
24
+ 3. šŸŽØ **Applies** `direction: rtl` and `unicode-bidi: plaintext` only where needed
25
+ 4. āœļø **Injects** the beautiful **Vazirmatn** font for perfect Persian/Arabic readability
26
+ 5. šŸ”’ **Isolates** English code blocks inside RTL text so they don't break
29
27
 
30
28
  ---
31
29
 
32
- ## šŸ“¦ Installation & Usage
33
-
34
- You don't even need to download the repository. Just run it directly via `npx`!
35
-
36
- ### 1. Patch the IDE
37
- To inject RTL support, run the following command in your terminal:
30
+ ## ⚔ Quick Start
38
31
 
39
32
  ```bash
40
- # macOS & Linux (Requires sudo to modify application files)
33
+ # Linux / macOS
34
+ npx antigravity-rtl-patcher patch
35
+
36
+ # If installed in system directory (e.g., /opt/)
41
37
  sudo npx antigravity-rtl-patcher patch
38
+ ```
42
39
 
43
- # Windows (Run PowerShell as Administrator)
40
+ ```bash
41
+ # Windows (Run terminal as Administrator if needed)
44
42
  npx antigravity-rtl-patcher patch
45
43
  ```
46
44
 
47
- ### 2. Restore (Unpatch)
48
- If you want to remove the patch or an update breaks something, you can easily restore from the backup:
45
+ That's it. Restart Antigravity IDE and enjoy native RTL support. ✨
46
+
47
+ ---
48
+
49
+ ## šŸ“– Commands
50
+
51
+ | Command | Description |
52
+ |---------|-------------|
53
+ | `agy-rtl patch` | Inject RTL support into Antigravity IDE |
54
+ | `agy-rtl restore` | Remove patch and restore original files |
55
+ | `agy-rtl status` | Check if IDE is currently patched |
56
+ | `agy-rtl patch --path /custom/path` | Patch a custom installation directory |
57
+
58
+ ---
59
+
60
+ ## šŸ”„ After IDE Updates
61
+
62
+ When Antigravity IDE updates, the patch may be overwritten. Simply run the patch command again:
49
63
 
50
64
  ```bash
51
- # macOS & Linux
52
- sudo npx antigravity-rtl-patcher restore
65
+ npx antigravity-rtl-patcher patch
66
+ ```
53
67
 
54
- # Windows
55
- npx antigravity-rtl-patcher restore
68
+ ---
69
+
70
+ ## šŸ—ļø Architecture
71
+
72
+ ```
73
+ antigravity-rtl-patcher/
74
+ ā”œā”€ā”€ bin/
75
+ │ └── cli.js # CLI entry point (commander-based)
76
+ ā”œā”€ā”€ src/
77
+ │ ā”œā”€ā”€ patcher.js # Core logic: backup, inject, repack
78
+ │ ā”œā”€ā”€ paths.js # OS-aware installation path detection
79
+ │ └── utils.js # Permission checks & helpers
80
+ ā”œā”€ā”€ payload/
81
+ │ ā”œā”€ā”€ rtl-engine.js # MutationObserver RTL detection engine
82
+ │ └── styles.css # Vazirmatn font + RTL typography fixes
83
+ ā”œā”€ā”€ package.json
84
+ └── README.md
56
85
  ```
57
86
 
58
- *(Note: Whenever Antigravity IDE updates, the patch will be overwritten. Just run the `patch` command again!)*
87
+ ### Supported Installation Types
88
+
89
+ | Type | Description | Method |
90
+ |------|-------------|--------|
91
+ | **Unpacked** | IDE with `resources/app/` directory | Direct file modification |
92
+ | **ASAR** | App bundled as `app.asar` | Extract → Patch → Repack |
59
93
 
60
94
  ---
61
95
 
62
- ## šŸ› ļø Architecture
96
+ ## šŸ” Permissions
97
+
98
+ - **User directory installs** (e.g., `~/Downloads/`): No special permissions needed
99
+ - **System directory installs** (e.g., `/opt/`): Requires `sudo` on Linux/macOS
63
100
 
64
- This project is divided into two main parts:
65
- 1. **The Patcher (`src/patcher.js`)**: A Node.js CLI that locates the `app.asar` file, extracts it to a temporary directory, injects our payload into the main `index.html`, and repacks it.
66
- 2. **The Payload (`payload/`)**: A combination of smart CSS and Vanilla JavaScript that runs inside the Electron UI. It uses a `MutationObserver` to watch for new chat messages dynamically generated by the AI and styles them on the fly.
101
+ ---
67
102
 
68
103
  ## šŸ¤ Contributing
104
+
69
105
  Contributions, issues, and feature requests are welcome!
70
- Feel free to check [issues page](https://github.com/VaFa1726/antigravity-rtl-patcher/issues).
106
+
107
+ 1. Fork the repo
108
+ 2. Create your feature branch (`git checkout -b feature/amazing`)
109
+ 3. Commit your changes (`git commit -m 'Add amazing feature'`)
110
+ 4. Push to the branch (`git push origin feature/amazing`)
111
+ 5. Open a Pull Request
71
112
 
72
113
  ## šŸ“„ License
73
- This project is [MIT](https://choosealicense.com/licenses/mit/) licensed.
74
114
 
75
- ---
76
- <div align="center">
77
- </div>
115
+ [MIT](https://choosealicense.com/licenses/mit/)
package/bin/cli.js CHANGED
@@ -2,20 +2,28 @@
2
2
 
3
3
  const { program } = require('commander');
4
4
  const chalk = require('chalk');
5
- const { patch, restore } = require('../src/patcher');
5
+ const { patch, restore, status } = require('../src/patcher');
6
+
7
+ const BANNER = `
8
+ ${chalk.cyan('╔══════════════════════════════════════════════╗')}
9
+ ${chalk.cyan('ā•‘')} ${chalk.bold.white('🌌 Antigravity RTL Patcher')} ${chalk.gray('v2.0.0')} ${chalk.cyan('ā•‘')}
10
+ ${chalk.cyan('ā•‘')} ${chalk.gray('Intelligent RTL support for Antigravity IDE')} ${chalk.cyan('ā•‘')}
11
+ ${chalk.cyan('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•')}
12
+ `;
6
13
 
7
14
  program
8
15
  .name('agy-rtl')
9
16
  .description('RTL & Typography patcher for Antigravity IDE')
10
- .version('1.0.0');
17
+ .version('2.0.0');
11
18
 
12
19
  program
13
20
  .command('patch')
14
21
  .description('Inject RTL support into Antigravity IDE')
15
- .action(async () => {
22
+ .option('-p, --path <path>', 'Custom path to Antigravity IDE installation')
23
+ .action(async (options) => {
24
+ console.log(BANNER);
16
25
  try {
17
- console.log(chalk.cyan.bold('\nšŸš€ Starting Antigravity RTL Patcher...'));
18
- await patch();
26
+ await patch(options.path);
19
27
  } catch (err) {
20
28
  console.error(chalk.red('\nāŒ Patch failed:'), err.message);
21
29
  process.exit(1);
@@ -25,14 +33,29 @@ program
25
33
  program
26
34
  .command('restore')
27
35
  .description('Restore Antigravity IDE to its original state')
28
- .action(async () => {
36
+ .option('-p, --path <path>', 'Custom path to Antigravity IDE installation')
37
+ .action(async (options) => {
38
+ console.log(BANNER);
29
39
  try {
30
- console.log(chalk.yellow.bold('\nšŸ”„ Restoring Antigravity...'));
31
- await restore();
40
+ await restore(options.path);
32
41
  } catch (err) {
33
42
  console.error(chalk.red('\nāŒ Restore failed:'), err.message);
34
43
  process.exit(1);
35
44
  }
36
45
  });
37
46
 
47
+ program
48
+ .command('status')
49
+ .description('Check if Antigravity IDE is currently patched')
50
+ .option('-p, --path <path>', 'Custom path to Antigravity IDE installation')
51
+ .action(async (options) => {
52
+ console.log(BANNER);
53
+ try {
54
+ await status(options.path);
55
+ } catch (err) {
56
+ console.error(chalk.red('\nāŒ Status check failed:'), err.message);
57
+ process.exit(1);
58
+ }
59
+ });
60
+
38
61
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,32 +1,50 @@
1
1
  {
2
2
  "name": "antigravity-rtl-patcher",
3
- "version": "1.0.0",
4
- "description": "An elegant RTL and typography patcher for Antigravity IDE based on Electron",
3
+ "version": "2.0.0",
4
+ "description": "🌌 Intelligent RTL & Persian/Arabic typography patcher for Antigravity IDE",
5
5
  "main": "src/patcher.js",
6
6
  "bin": {
7
+ "antigravity-rtl-patcher": "./bin/cli.js",
7
8
  "agy-rtl": "./bin/cli.js"
8
9
  },
9
10
  "scripts": {
10
- "start": "node ./bin/cli.js"
11
+ "start": "node ./bin/cli.js",
12
+ "patch": "node ./bin/cli.js patch",
13
+ "restore": "node ./bin/cli.js restore"
11
14
  },
12
15
  "keywords": [
13
16
  "antigravity",
14
17
  "rtl",
15
18
  "farsi",
19
+ "persian",
16
20
  "arabic",
17
21
  "hebrew",
18
22
  "patcher",
19
23
  "electron",
20
- "asar"
24
+ "asar",
25
+ "ide",
26
+ "vscode",
27
+ "right-to-left",
28
+ "bidirectional"
21
29
  ],
22
30
  "author": "VaFa1726",
23
31
  "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/VaFa1726/antigravity-rtl-patcher.git"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/VaFa1726/antigravity-rtl-patcher/issues"
38
+ },
39
+ "homepage": "https://github.com/VaFa1726/antigravity-rtl-patcher#readme",
40
+ "engines": {
41
+ "node": ">=16.0.0"
42
+ },
24
43
  "dependencies": {
25
44
  "@electron/asar": "^3.2.14",
26
45
  "chalk": "^4.1.2",
27
46
  "commander": "^11.1.0",
28
47
  "fs-extra": "^11.2.0",
29
- "ora": "^5.4.1",
30
- "sudo-prompt": "^9.2.1"
48
+ "ora": "^5.4.1"
31
49
  }
32
50
  }
@@ -1,73 +1,156 @@
1
- (function() {
2
- console.log('šŸš€ AGY RTL Engine Initialized');
1
+ /**
2
+ * AGY RTL Engine v2.0.0
3
+ * Intelligent RTL detection & styling for Antigravity IDE
4
+ *
5
+ * This script runs inside the Electron renderer process.
6
+ * It uses MutationObserver to watch for DOM changes and
7
+ * automatically applies RTL direction to Persian/Arabic/Hebrew text.
8
+ *
9
+ * CSP Compatible: No use of innerHTML, eval, or inline event handlers.
10
+ */
11
+ (function () {
12
+ 'use strict';
3
13
 
4
- // Regex to detect RTL characters (Arabic, Persian, Hebrew)
5
- const rtlRegex = /[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/;
14
+ // Unicode ranges for RTL scripts
15
+ const RTL_REGEX = /[\u0591-\u07FF\u200F\u202B\u202E\uFB1D-\uFDFD\uFE70-\uFEFC]/;
6
16
 
17
+ // Minimum ratio of RTL characters to consider text as RTL
18
+ const RTL_CHAR_REGEX = /[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/g;
19
+ const MIN_RTL_RATIO = 0.3;
20
+
21
+ // Attribute to mark processed elements
22
+ const PROCESSED_ATTR = 'data-agy-rtl';
23
+
24
+ // Selectors to target (chat messages, markdown output, editor tooltips)
25
+ const TARGET_SELECTORS = [
26
+ // Chat and AI response areas
27
+ '.chat-response-content p',
28
+ '.chat-response-content li',
29
+ '.chat-response-content h1',
30
+ '.chat-response-content h2',
31
+ '.chat-response-content h3',
32
+ '.chat-response-content h4',
33
+ '.chat-response-content blockquote',
34
+ // Markdown preview
35
+ '.markdown-body p',
36
+ '.markdown-body li',
37
+ '.markdown-body h1',
38
+ '.markdown-body h2',
39
+ '.markdown-body h3',
40
+ '.markdown-body blockquote',
41
+ // Generic rendered text
42
+ '.rendered-markdown p',
43
+ '.rendered-markdown li',
44
+ '.rendered-markdown h1',
45
+ '.rendered-markdown h2',
46
+ '.rendered-markdown h3',
47
+ '.rendered-markdown blockquote',
48
+ // Monaco editor hover & tooltips
49
+ '.monaco-hover-content p',
50
+ '.suggest-details p',
51
+ // Notifications
52
+ '.notification-toast-container p',
53
+ // Generic paragraphs and spans in webview
54
+ 'p', 'li', 'h1', 'h2', 'h3', 'h4', 'blockquote',
55
+ ].join(', ');
56
+
57
+ /**
58
+ * Determine if a text string is primarily RTL.
59
+ */
7
60
  function isRTL(text) {
8
- // Basic heuristic: check if any RTL char exists
9
- return rtlRegex.test(text);
61
+ if (!text || text.trim().length === 0) return false;
62
+
63
+ // Quick check: does it contain any RTL character at all?
64
+ if (!RTL_REGEX.test(text)) return false;
65
+
66
+ // Count RTL characters vs total alphabetic characters
67
+ const rtlMatches = text.match(RTL_CHAR_REGEX);
68
+ if (!rtlMatches) return false;
69
+
70
+ const alphaChars = text.replace(/[\s\d\W]/g, '').length;
71
+ if (alphaChars === 0) return false;
72
+
73
+ return (rtlMatches.length / alphaChars) >= MIN_RTL_RATIO;
10
74
  }
11
75
 
76
+ /**
77
+ * Apply RTL styling to an element.
78
+ */
12
79
  function applyRTL(element) {
13
- // Only process text nodes or direct containers
14
- const textContent = element.textContent || element.innerText || '';
15
-
16
- if (isRTL(textContent)) {
17
- element.setAttribute('data-agy-rtl', 'true');
80
+ if (element.getAttribute(PROCESSED_ATTR)) return;
81
+
82
+ const text = element.textContent || '';
83
+ if (isRTL(text)) {
84
+ element.setAttribute(PROCESSED_ATTR, 'true');
18
85
  element.style.direction = 'rtl';
19
86
  element.style.textAlign = 'start';
20
-
21
- // Crucial for mixed English/Farsi content (like code snippets in Farsi text)
22
- element.style.unicodeBidi = 'plaintext';
23
-
24
- // Mark as processed
25
- element.classList.add('agy-rtl-processed');
87
+ element.style.unicodeBidi = 'plaintext';
26
88
  }
27
89
  }
28
90
 
29
- // Observe DOM mutations for dynamically added chat messages
30
- const observer = new MutationObserver((mutations) => {
31
- mutations.forEach((mutation) => {
32
- if (mutation.addedNodes.length) {
33
- mutation.addedNodes.forEach((node) => {
91
+ /**
92
+ * Scan a subtree for elements that need RTL.
93
+ */
94
+ function scanElement(root) {
95
+ if (!root || root.nodeType !== Node.ELEMENT_NODE) return;
96
+
97
+ // Check if the root itself matches
98
+ try {
99
+ if (root.matches && root.matches(TARGET_SELECTORS)) {
100
+ applyRTL(root);
101
+ }
102
+ } catch (e) {
103
+ // Ignore selector errors
104
+ }
105
+
106
+ // Scan children
107
+ try {
108
+ const elements = root.querySelectorAll(TARGET_SELECTORS);
109
+ elements.forEach(function (el) {
110
+ applyRTL(el);
111
+ });
112
+ } catch (e) {
113
+ // Ignore errors
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Initialize the MutationObserver.
119
+ */
120
+ function initObserver() {
121
+ var observer = new MutationObserver(function (mutations) {
122
+ mutations.forEach(function (mutation) {
123
+ // Handle added nodes
124
+ mutation.addedNodes.forEach(function (node) {
34
125
  if (node.nodeType === Node.ELEMENT_NODE) {
35
- // Apply logic to the node itself if it has text
36
- if (node.textContent && node.textContent.trim().length > 0) {
37
- // We should probably target specific chat bubble selectors here if we knew them.
38
- // For a generic approach, we look for paragraphs or divs with raw text.
39
- const elementsToScan = node.querySelectorAll('p, span, div.message-content, div.chat-bubble, li, h1, h2, h3');
40
- elementsToScan.forEach(el => {
41
- if (!el.classList.contains('agy-rtl-processed')) {
42
- applyRTL(el);
43
- }
44
- });
45
-
46
- // Also check the root node added
47
- if (!node.classList.contains('agy-rtl-processed')) {
48
- applyRTL(node);
49
- }
50
- }
126
+ scanElement(node);
51
127
  }
52
128
  });
53
- }
129
+
130
+ // Handle text content changes
131
+ if (mutation.type === 'characterData' && mutation.target.parentElement) {
132
+ applyRTL(mutation.target.parentElement);
133
+ }
134
+ });
54
135
  });
55
- });
56
136
 
57
- // Start observing
58
- window.addEventListener('load', () => {
59
- observer.observe(document.body, {
60
- childList: true,
137
+ observer.observe(document.body, {
138
+ childList: true,
61
139
  subtree: true,
62
- characterData: true
140
+ characterData: true,
63
141
  });
64
-
65
- // Initial scan
66
- document.querySelectorAll('p, span, div, li, h1, h2, h3').forEach(el => {
67
- if (el.textContent && el.textContent.trim().length > 0 && !el.children.length) {
68
- applyRTL(el);
69
- }
70
- });
71
- });
72
142
 
143
+ // Initial full-page scan
144
+ scanElement(document.body);
145
+ }
146
+
147
+ // Start when DOM is ready
148
+ if (document.readyState === 'loading') {
149
+ document.addEventListener('DOMContentLoaded', initObserver);
150
+ } else {
151
+ // DOM already loaded, wait a moment for dynamic content
152
+ setTimeout(initObserver, 500);
153
+ }
154
+
155
+ console.log('%c🌌 AGY RTL Engine v2.0.0 loaded', 'color: cyan; font-weight: bold;');
73
156
  })();
@@ -1,20 +1,37 @@
1
- /* Import a modern Persian font from Google Fonts (Vazirmatn) */
1
+ /*
2
+ * AGY RTL Styles v2.0.0
3
+ * Typography & RTL fixes for Antigravity IDE
4
+ */
5
+
6
+ /* Vazirmatn — Modern Persian/Arabic font from Google Fonts */
2
7
  @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700&display=swap');
3
8
 
4
- /* Apply font to patched elements */
9
+ /* RTL text styling */
5
10
  [data-agy-rtl="true"] {
6
- font-family: 'Vazirmatn', system-ui, -apple-system, sans-serif !important;
7
- line-height: 1.8 !important;
8
- letter-spacing: normal !important;
11
+ font-family: 'Vazirmatn', system-ui, -apple-system, 'Segoe UI', sans-serif !important;
12
+ line-height: 1.85 !important;
13
+ word-spacing: 0.02em;
9
14
  }
10
15
 
11
- /* Fix code blocks inside RTL text */
12
- [data-agy-rtl="true"] code,
13
- [data-agy-rtl="true"] pre {
16
+ /* Preserve LTR for code blocks inside RTL text */
17
+ [data-agy-rtl="true"] code,
18
+ [data-agy-rtl="true"] pre,
19
+ [data-agy-rtl="true"] .monaco-tokenized-source {
14
20
  direction: ltr !important;
15
21
  text-align: left !important;
16
22
  unicode-bidi: isolate !important;
17
- font-family: monospace !important;
23
+ font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', 'Consolas', monospace !important;
18
24
  display: inline-block;
19
- margin: 0 4px;
25
+ }
26
+
27
+ /* Inline code in RTL paragraphs */
28
+ [data-agy-rtl="true"] code {
29
+ margin: 0 3px;
30
+ padding: 1px 5px;
31
+ border-radius: 3px;
32
+ }
33
+
34
+ /* Fix list markers in RTL */
35
+ [data-agy-rtl="true"] li {
36
+ list-style-position: inside;
20
37
  }
package/src/patcher.js CHANGED
@@ -4,57 +4,103 @@ const path = require('path');
4
4
  const os = require('os');
5
5
  const ora = require('ora');
6
6
  const chalk = require('chalk');
7
- const { getAsarPath } = require('./paths');
7
+ const { findInstallations } = require('./paths');
8
8
  const { checkPermissions, delay } = require('./utils');
9
9
 
10
- async function patch() {
11
- checkPermissions();
12
-
13
- const spinner = ora('Locating Antigravity IDE...').start();
14
- let asarPath;
15
-
16
- try {
17
- asarPath = getAsarPath();
18
- spinner.succeed(`Found Antigravity at: ${chalk.gray(asarPath)}`);
19
- } catch (err) {
20
- spinner.fail(err.message);
21
- throw err;
10
+ const PATCH_MARKER = 'agy-rtl-engine';
11
+ const BACKUP_SUFFIX = '.agy-rtl-backup';
12
+ const ENGINE_FILENAME = 'agy-rtl-engine.js';
13
+ const STYLES_FILENAME = 'agy-rtl-styles.css';
14
+
15
+ /**
16
+ * Patch an unpacked Antigravity IDE installation.
17
+ * Copies payload files and modifies workbench.html.
18
+ */
19
+ async function patchUnpacked(installation, spinner) {
20
+ const { workbenchDir, workbenchHtml } = installation;
21
+
22
+ // 1. Backup
23
+ const backupPath = workbenchHtml + BACKUP_SUFFIX;
24
+ if (!fs.existsSync(backupPath)) {
25
+ await fs.copy(workbenchHtml, backupPath);
26
+ spinner.succeed('Backup created: ' + chalk.gray(path.basename(backupPath)));
27
+ } else {
28
+ spinner.info('Backup already exists, skipping.');
22
29
  }
23
30
 
24
- const backupPath = `${asarPath}.backup`;
25
- const tmpDir = path.join(os.tmpdir(), 'agy-patch-tmp');
26
-
31
+ // 2. Check if already patched
32
+ spinner.start('Checking patch status...');
33
+ const htmlContent = await fs.readFile(workbenchHtml, 'utf-8');
34
+ if (htmlContent.includes(PATCH_MARKER)) {
35
+ spinner.info('IDE is already patched. Use "restore" first to re-patch.');
36
+ return;
37
+ }
38
+
39
+ // 3. Copy payload files to workbench directory
40
+ spinner.start('Copying RTL engine & styles...');
41
+ const payloadDir = path.join(__dirname, '..', 'payload');
42
+
43
+ await fs.copy(
44
+ path.join(payloadDir, 'rtl-engine.js'),
45
+ path.join(workbenchDir, ENGINE_FILENAME)
46
+ );
47
+ await fs.copy(
48
+ path.join(payloadDir, 'styles.css'),
49
+ path.join(workbenchDir, STYLES_FILENAME)
50
+ );
51
+ spinner.succeed('Payload files copied.');
52
+
53
+ // 4. Inject references into workbench.html
54
+ spinner.start('Injecting RTL support into workbench...');
55
+ let modified = htmlContent;
56
+
57
+ // Add stylesheet link in <head> before </head>
58
+ const styleTag = `\n\t<!-- AGY-RTL-PATCH -->\n\t<link rel="stylesheet" href="./${STYLES_FILENAME}">\n\t<!-- /AGY-RTL-PATCH -->`;
59
+ modified = modified.replace('</head>', `${styleTag}\n</head>`);
60
+
61
+ // Add script before </html>
62
+ const scriptTag = `\n<!-- AGY-RTL-PATCH -->\n<script src="./${ENGINE_FILENAME}" id="${PATCH_MARKER}"></script>\n<!-- /AGY-RTL-PATCH -->`;
63
+ modified = modified.replace('</html>', `${scriptTag}\n</html>`);
64
+
65
+ await fs.writeFile(workbenchHtml, modified, 'utf-8');
66
+ spinner.succeed('RTL support injected into workbench.');
67
+ }
68
+
69
+ /**
70
+ * Patch an ASAR-packed Antigravity installation.
71
+ * Extracts the asar, patches files, and repacks.
72
+ */
73
+ async function patchAsar(installation, spinner) {
74
+ const { asarPath } = installation;
75
+ const backupPath = asarPath + BACKUP_SUFFIX;
76
+ const tmpDir = path.join(os.tmpdir(), 'agy-rtl-patch-' + Date.now());
77
+
27
78
  try {
28
79
  // 1. Backup
29
- spinner.start('Creating backup...');
30
80
  if (!fs.existsSync(backupPath)) {
81
+ spinner.start('Creating backup...');
31
82
  await fs.copy(asarPath, backupPath);
32
- spinner.succeed('Backup created successfully.');
83
+ spinner.succeed('Backup created.');
33
84
  } else {
34
85
  spinner.info('Backup already exists, skipping.');
35
86
  }
36
87
 
37
88
  // 2. Extract
38
- spinner.start('Extracting core files...');
39
- if (fs.existsSync(tmpDir)) {
40
- await fs.remove(tmpDir);
41
- }
89
+ spinner.start('Extracting app.asar...');
42
90
  await fs.ensureDir(tmpDir);
43
91
  asar.extractAll(asarPath, tmpDir);
44
- spinner.succeed('Core files extracted.');
92
+ spinner.succeed('Extracted successfully.');
45
93
 
46
- // 3. Inject Payload
47
- spinner.start('Injecting RTL engine & typography...');
48
-
49
- // Find the main HTML file - usually index.html in the root or a dist folder
50
- const possibleHtmlPaths = [
94
+ // 3. Find workbench.html inside extracted asar
95
+ const possiblePaths = [
96
+ path.join(tmpDir, 'out', 'vs', 'code', 'electron-browser', 'workbench', 'workbench.html'),
51
97
  path.join(tmpDir, 'index.html'),
98
+ path.join(tmpDir, 'dist', 'index.html'),
52
99
  path.join(tmpDir, 'out', 'index.html'),
53
- path.join(tmpDir, 'dist', 'index.html')
54
100
  ];
55
-
101
+
56
102
  let targetHtml = null;
57
- for (const p of possibleHtmlPaths) {
103
+ for (const p of possiblePaths) {
58
104
  if (fs.existsSync(p)) {
59
105
  targetHtml = p;
60
106
  break;
@@ -62,90 +108,197 @@ async function patch() {
62
108
  }
63
109
 
64
110
  if (!targetHtml) {
65
- throw new Error('Could not find the main HTML file to inject.');
111
+ spinner.warn('No patchable HTML found in this package. Skipping.');
112
+ await fs.remove(tmpDir);
113
+ return;
66
114
  }
67
115
 
68
- let htmlContent = await fs.readFile(targetHtml, 'utf-8');
69
-
70
- // Read payload files
71
- const engineCode = await fs.readFile(path.join(__dirname, '..', 'payload', 'rtl-engine.js'), 'utf-8');
72
- const stylesCode = await fs.readFile(path.join(__dirname, '..', 'payload', 'styles.css'), 'utf-8');
73
-
74
116
  // Check if already patched
75
- if (htmlContent.includes('agy-rtl-engine')) {
76
- spinner.info('IDE is already patched.');
117
+ const htmlContent = await fs.readFile(targetHtml, 'utf-8');
118
+ if (htmlContent.includes(PATCH_MARKER)) {
119
+ spinner.info('ASAR is already patched.');
77
120
  await fs.remove(tmpDir);
78
121
  return;
79
122
  }
80
123
 
81
- const injection = `
82
- <!-- AGY RTL PATCH -->
83
- <style id="agy-rtl-styles">${stylesCode}</style>
84
- <script id="agy-rtl-engine">${engineCode}</script>
85
- <!-- END AGY RTL PATCH -->
86
- `;
124
+ // 4. Copy payload files
125
+ spinner.start('Injecting RTL engine...');
126
+ const payloadDir = path.join(__dirname, '..', 'payload');
127
+ const targetDir = path.dirname(targetHtml);
128
+
129
+ await fs.copy(path.join(payloadDir, 'rtl-engine.js'), path.join(targetDir, ENGINE_FILENAME));
130
+ await fs.copy(path.join(payloadDir, 'styles.css'), path.join(targetDir, STYLES_FILENAME));
131
+
132
+ // 5. Modify HTML
133
+ let modified = htmlContent;
134
+ const styleTag = `\n\t<!-- AGY-RTL-PATCH -->\n\t<link rel="stylesheet" href="./${STYLES_FILENAME}">\n\t<!-- /AGY-RTL-PATCH -->`;
135
+ const scriptTag = `\n<!-- AGY-RTL-PATCH -->\n<script src="./${ENGINE_FILENAME}" id="${PATCH_MARKER}"></script>\n<!-- /AGY-RTL-PATCH -->`;
87
136
 
88
- // Inject just before </body>
89
- if (htmlContent.includes('</body>')) {
90
- htmlContent = htmlContent.replace('</body>', `${injection}\n</body>`);
137
+ if (modified.includes('</head>')) {
138
+ modified = modified.replace('</head>', `${styleTag}\n</head>`);
139
+ }
140
+ if (modified.includes('</html>')) {
141
+ modified = modified.replace('</html>', `${scriptTag}\n</html>`);
142
+ } else if (modified.includes('</body>')) {
143
+ modified = modified.replace('</body>', `${scriptTag}\n</body>`);
91
144
  } else {
92
- // Fallback
93
- htmlContent += injection;
145
+ modified += scriptTag;
94
146
  }
95
147
 
96
- await fs.writeFile(targetHtml, htmlContent);
97
- spinner.succeed('Payload injected into UI.');
148
+ await fs.writeFile(targetHtml, modified, 'utf-8');
149
+ spinner.succeed('RTL engine injected.');
98
150
 
99
- // 4. Repack
100
- spinner.start('Repacking core files...');
101
- await delay(500); // Give file system a moment
151
+ // 6. Repack
152
+ spinner.start('Repacking app.asar...');
153
+ await delay(300);
102
154
  await asar.createPackage(tmpDir, asarPath);
103
- spinner.succeed('Core repacked successfully.');
104
-
105
- // 5. Cleanup
106
- spinner.start('Cleaning up...');
107
- await fs.remove(tmpDir);
108
- spinner.succeed('Cleanup complete.');
155
+ spinner.succeed('Repacked successfully.');
109
156
 
110
- console.log(chalk.green.bold('\n✨ Antigravity IDE successfully patched with RTL support!'));
111
- console.log(chalk.cyan('Please restart Antigravity for the changes to take effect.\n'));
112
-
113
- } catch (err) {
114
- spinner.fail('An error occurred during patching.');
157
+ } finally {
158
+ // Cleanup temp dir
115
159
  if (fs.existsSync(tmpDir)) {
116
160
  await fs.remove(tmpDir);
117
161
  }
118
- throw err;
119
162
  }
120
163
  }
121
164
 
122
- async function restore() {
123
- checkPermissions();
124
- const spinner = ora('Locating Antigravity IDE...').start();
125
-
126
- let asarPath;
127
- try {
128
- asarPath = getAsarPath();
129
- spinner.succeed(`Found Antigravity at: ${chalk.gray(asarPath)}`);
130
- } catch (err) {
131
- spinner.fail(err.message);
132
- throw err;
165
+ /**
166
+ * Main patch function.
167
+ */
168
+ async function patch(customPath) {
169
+ const spinner = ora('Searching for Antigravity IDE...').start();
170
+
171
+ const installations = findInstallations(customPath);
172
+
173
+ if (installations.length === 0) {
174
+ spinner.fail('Antigravity IDE installation not found.');
175
+ console.error(chalk.yellow('\nTry specifying the path manually:'));
176
+ console.error(chalk.cyan(' agy-rtl patch --path /path/to/Antigravity-IDE\n'));
177
+ throw new Error('Installation not found.');
178
+ }
179
+
180
+ spinner.succeed(`Found ${installations.length} installation(s).`);
181
+
182
+ for (const inst of installations) {
183
+ console.log(chalk.cyan(`\nšŸ“‚ Patching: ${chalk.white(inst.basePath)}`));
184
+ console.log(chalk.gray(` Type: ${inst.type === 'unpacked' ? 'Unpacked App' : 'ASAR Package'}`));
185
+
186
+ checkPermissions(inst.basePath);
187
+
188
+ if (inst.type === 'unpacked') {
189
+ await patchUnpacked(inst, ora());
190
+ } else {
191
+ await patchAsar(inst, ora());
192
+ }
193
+ }
194
+
195
+ console.log(chalk.green.bold('\n✨ Antigravity IDE successfully patched with RTL support!'));
196
+ console.log(chalk.cyan(' Please restart Antigravity IDE for changes to take effect.\n'));
197
+ }
198
+
199
+ /**
200
+ * Restore an unpacked installation from backup.
201
+ */
202
+ async function restoreUnpacked(installation, spinner) {
203
+ const { workbenchDir, workbenchHtml } = installation;
204
+ const backupPath = workbenchHtml + BACKUP_SUFFIX;
205
+
206
+ if (fs.existsSync(backupPath)) {
207
+ spinner.start('Restoring workbench.html from backup...');
208
+ await fs.copy(backupPath, workbenchHtml);
209
+ await fs.remove(backupPath);
210
+ spinner.succeed('Workbench restored.');
211
+ } else {
212
+ spinner.warn('No backup found for workbench.html.');
133
213
  }
134
214
 
135
- const backupPath = `${asarPath}.backup`;
136
-
137
- spinner.start('Restoring from backup...');
215
+ // Remove injected payload files
216
+ const engineFile = path.join(workbenchDir, ENGINE_FILENAME);
217
+ const stylesFile = path.join(workbenchDir, STYLES_FILENAME);
218
+
219
+ if (fs.existsSync(engineFile)) await fs.remove(engineFile);
220
+ if (fs.existsSync(stylesFile)) await fs.remove(stylesFile);
221
+ spinner.succeed('Payload files removed.');
222
+ }
223
+
224
+ /**
225
+ * Restore an ASAR installation from backup.
226
+ */
227
+ async function restoreAsar(installation, spinner) {
228
+ const { asarPath } = installation;
229
+ const backupPath = asarPath + BACKUP_SUFFIX;
230
+
138
231
  if (fs.existsSync(backupPath)) {
232
+ spinner.start('Restoring app.asar from backup...');
139
233
  await fs.copy(backupPath, asarPath);
140
234
  await fs.remove(backupPath);
141
- spinner.succeed('Restored successfully.');
142
- console.log(chalk.green.bold('\n✨ Antigravity IDE restored to original state!'));
235
+ spinner.succeed('ASAR restored.');
143
236
  } else {
144
- spinner.fail('No backup found. The IDE might not be patched or backup was removed.');
237
+ spinner.warn('No backup found for app.asar.');
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Main restore function.
243
+ */
244
+ async function restore(customPath) {
245
+ const spinner = ora('Searching for Antigravity IDE...').start();
246
+ const installations = findInstallations(customPath);
247
+
248
+ if (installations.length === 0) {
249
+ spinner.fail('Antigravity IDE installation not found.');
250
+ throw new Error('Installation not found.');
251
+ }
252
+
253
+ spinner.succeed(`Found ${installations.length} installation(s).`);
254
+
255
+ for (const inst of installations) {
256
+ console.log(chalk.yellow(`\nšŸ”„ Restoring: ${chalk.white(inst.basePath)}`));
257
+
258
+ checkPermissions(inst.basePath);
259
+
260
+ if (inst.type === 'unpacked') {
261
+ await restoreUnpacked(inst, ora());
262
+ } else {
263
+ await restoreAsar(inst, ora());
264
+ }
265
+ }
266
+
267
+ console.log(chalk.green.bold('\n✨ Antigravity IDE restored to original state!'));
268
+ console.log(chalk.cyan(' Please restart Antigravity IDE for changes to take effect.\n'));
269
+ }
270
+
271
+ /**
272
+ * Check patch status.
273
+ */
274
+ async function status(customPath) {
275
+ const spinner = ora('Searching for Antigravity IDE...').start();
276
+ const installations = findInstallations(customPath);
277
+
278
+ if (installations.length === 0) {
279
+ spinner.fail('Antigravity IDE installation not found.');
280
+ return;
281
+ }
282
+
283
+ spinner.succeed(`Found ${installations.length} installation(s).`);
284
+
285
+ for (const inst of installations) {
286
+ let isPatched = false;
287
+
288
+ if (inst.type === 'unpacked') {
289
+ const html = await fs.readFile(inst.workbenchHtml, 'utf-8');
290
+ isPatched = html.includes(PATCH_MARKER);
291
+ }
292
+
293
+ const statusIcon = isPatched ? chalk.green('ā— PATCHED') : chalk.red('ā—‹ NOT PATCHED');
294
+ console.log(`\n ${statusIcon} ${chalk.white(inst.basePath)}`);
295
+ console.log(chalk.gray(` Type: ${inst.type === 'unpacked' ? 'Unpacked App' : 'ASAR Package'}`));
145
296
  }
297
+ console.log('');
146
298
  }
147
299
 
148
300
  module.exports = {
149
301
  patch,
150
- restore
302
+ restore,
303
+ status,
151
304
  };
package/src/paths.js CHANGED
@@ -2,36 +2,116 @@ const fs = require('fs-extra');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
4
 
5
- function getAsarPath() {
5
+ /**
6
+ * Known installation paths for Antigravity IDE across all platforms.
7
+ * Both the IDE (unpacked) and the standalone app (asar) are supported.
8
+ */
9
+ function getSearchPaths() {
10
+ const home = os.homedir();
6
11
  const platform = os.platform();
7
-
8
- // Possible paths where the IDE might be installed
9
- const paths = {
10
- win32: [
11
- path.join(process.env.LOCALAPPDATA || '', 'Programs', 'antigravity', 'resources', 'app.asar'),
12
- path.join(process.env.PROGRAMFILES || '', 'antigravity', 'resources', 'app.asar')
12
+
13
+ const common = [
14
+ // Downloads (common for manual installs)
15
+ path.join(home, 'Downloads', 'Antigravity-IDE'),
16
+ path.join(home, 'Downloads', 'Antigravity-x64'),
17
+ path.join(home, 'Downloads', 'Antigravity'),
18
+ // Desktop
19
+ path.join(home, 'Desktop', 'Antigravity-IDE'),
20
+ path.join(home, 'Desktop', 'Antigravity-x64'),
21
+ ];
22
+
23
+ const platformPaths = {
24
+ linux: [
25
+ ...common,
26
+ '/opt/antigravity-ide',
27
+ '/opt/Antigravity-IDE',
28
+ '/opt/antigravity',
29
+ '/usr/lib/antigravity-ide',
30
+ '/usr/share/antigravity-ide',
31
+ path.join(home, '.local', 'share', 'antigravity-ide'),
32
+ path.join(home, '.local', 'lib', 'antigravity-ide'),
13
33
  ],
14
34
  darwin: [
15
- '/Applications/Antigravity.app/Contents/Resources/app.asar',
16
- path.join(os.homedir(), 'Applications', 'Antigravity.app', 'Contents', 'Resources', 'app.asar')
35
+ '/Applications/Antigravity IDE.app/Contents',
36
+ path.join(home, 'Applications', 'Antigravity IDE.app', 'Contents'),
37
+ '/Applications/Antigravity.app/Contents',
38
+ ...common,
39
+ ],
40
+ win32: [
41
+ path.join(process.env.LOCALAPPDATA || '', 'Programs', 'antigravity-ide'),
42
+ path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Antigravity IDE'),
43
+ path.join(process.env.PROGRAMFILES || '', 'Antigravity IDE'),
44
+ path.join(process.env.LOCALAPPDATA || '', 'Programs', 'antigravity'),
45
+ ...common,
17
46
  ],
18
- linux: [
19
- '/opt/antigravity/resources/app.asar',
20
- '/usr/local/lib/antigravity/resources/app.asar'
21
- ]
22
47
  };
23
48
 
24
- const possiblePaths = paths[platform] || [];
49
+ return platformPaths[platform] || common;
50
+ }
51
+
52
+ /**
53
+ * Detect the type of Antigravity installation.
54
+ * Returns: { type: 'unpacked' | 'asar', basePath, workbenchDir, workbenchHtml }
55
+ */
56
+ function detectInstallation(basePath) {
57
+ // Type 1: Unpacked app directory (IDE version)
58
+ const unpackedApp = path.join(basePath, 'resources', 'app');
59
+ const workbenchUnpacked = path.join(
60
+ unpackedApp,
61
+ 'out', 'vs', 'code', 'electron-browser', 'workbench', 'workbench.html'
62
+ );
63
+
64
+ if (fs.existsSync(workbenchUnpacked)) {
65
+ return {
66
+ type: 'unpacked',
67
+ basePath,
68
+ appDir: unpackedApp,
69
+ workbenchDir: path.dirname(workbenchUnpacked),
70
+ workbenchHtml: workbenchUnpacked,
71
+ };
72
+ }
73
+
74
+ // Type 2: ASAR packed app
75
+ const asarPath = path.join(basePath, 'resources', 'app.asar');
76
+ if (fs.existsSync(asarPath)) {
77
+ return {
78
+ type: 'asar',
79
+ basePath,
80
+ asarPath,
81
+ };
82
+ }
83
+
84
+ return null;
85
+ }
86
+
87
+ /**
88
+ * Find all Antigravity IDE installations on the system.
89
+ */
90
+ function findInstallations(customPath) {
91
+ const results = [];
92
+
93
+ if (customPath) {
94
+ const info = detectInstallation(customPath);
95
+ if (info) {
96
+ results.push(info);
97
+ }
98
+ return results;
99
+ }
25
100
 
26
- for (const p of possiblePaths) {
27
- if (fs.existsSync(p)) {
28
- return p;
101
+ const searchPaths = getSearchPaths();
102
+ for (const sp of searchPaths) {
103
+ if (fs.existsSync(sp)) {
104
+ const info = detectInstallation(sp);
105
+ if (info) {
106
+ results.push(info);
107
+ }
29
108
  }
30
109
  }
31
110
 
32
- throw new Error('Antigravity installation not found. Please ensure it is installed in the default location.');
111
+ return results;
33
112
  }
34
113
 
35
114
  module.exports = {
36
- getAsarPath
115
+ findInstallations,
116
+ detectInstallation,
37
117
  };
package/src/utils.js CHANGED
@@ -1,26 +1,35 @@
1
1
  const chalk = require('chalk');
2
2
 
3
- function checkPermissions() {
3
+ /**
4
+ * Check if the current process has sufficient permissions
5
+ * to modify system-level application files.
6
+ */
7
+ function checkPermissions(targetPath) {
4
8
  const isWindows = process.platform === 'win32';
5
-
9
+
6
10
  if (!isWindows) {
7
- // Check if running as root
8
11
  const isRoot = process.getuid && process.getuid() === 0;
9
- if (!isRoot) {
12
+ // Check if the target is in a user-writable directory
13
+ const isUserDir = targetPath && targetPath.startsWith(require('os').homedir());
14
+
15
+ if (!isRoot && !isUserDir) {
10
16
  console.error(chalk.red.bold('\nāš ļø Permission Denied!'));
11
17
  console.error(chalk.white('Modifying system applications requires administrator privileges.'));
12
- console.error(chalk.white(`Please run the command with ${chalk.yellow('sudo')}:\n`));
13
- console.error(chalk.cyan(' sudo npx agy-rtl patch\n'));
18
+ console.error(chalk.white(`Please run with ${chalk.yellow('sudo')}:\n`));
19
+ console.error(chalk.cyan(' sudo npx antigravity-rtl-patcher patch\n'));
14
20
  process.exit(1);
15
21
  }
16
22
  }
17
23
  }
18
24
 
25
+ /**
26
+ * Simple async delay helper.
27
+ */
19
28
  function delay(ms) {
20
- return new Promise(resolve => setTimeout(resolve, ms));
29
+ return new Promise((resolve) => setTimeout(resolve, ms));
21
30
  }
22
31
 
23
32
  module.exports = {
24
33
  checkPermissions,
25
- delay
34
+ delay,
26
35
  };