antigravity-rtl-patch 1.0.2

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/CHANGELOG.md ADDED
@@ -0,0 +1,61 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] - 2026-09-06
9
+
10
+ ### šŸŽ‰ First Stable Release
11
+
12
+ This is the first stable and production-ready release of Antigravity RTL Patcher.
13
+
14
+ ### ✨ Features
15
+
16
+ - **Smart RTL Detection**: Automatically detects and applies RTL to Persian/Arabic text
17
+ - **RTL Support for All Content Types**:
18
+ - Messages and chat content
19
+ - Tables and data displays
20
+ - Code blocks with Persian comments
21
+ - Headings, paragraphs, and lists
22
+ - **Preserved UI Layout**: Sidebar, navigation, and toolbars stay LTR
23
+ - **Toggle Control**: Easy on/off switch with visual indicator
24
+ - **Keyboard Shortcut**: `Alt + R` to quickly toggle RTL mode
25
+ - **Persistent Settings**: Configuration saved to `~/.antigravity-rtl.json`
26
+ - **System Font Stack**: Uses native fonts (Segoe UI, Tahoma, Arial) for optimal rendering
27
+ - **Proper Character Joining**: Persian/Arabic characters connect correctly
28
+
29
+ ### šŸ”§ Technical Details
30
+
31
+ - Injects into Antigravity's `dist/utils.js` for reliable operation
32
+ - CSS-based RTL implementation with selective targeting
33
+ - Automatic backup before patching
34
+ - Clean restore functionality
35
+
36
+ ### šŸ“¦ Installation
37
+
38
+ ```bash
39
+ npx antigravity-rtl-patcher@latest patch
40
+ ```
41
+
42
+ ### šŸŽÆ Compatibility
43
+
44
+ - Supports all major Antigravity versions
45
+ - Works on Linux, macOS, and Windows
46
+ - Automatically detects installation paths
47
+
48
+ ### šŸ™ Credits
49
+
50
+ - Persian font rendering improvements
51
+ - Community feedback and testing
52
+ - Built with ā¤ļø for Persian/Arabic users
53
+
54
+ ---
55
+
56
+ ## Development History
57
+
58
+ Previous versions (2.x, 3.x) were experimental releases during active development.
59
+ Version 1.0.0 represents the first stable, production-ready release.
60
+
61
+ [1.0.0]: https://github.com/VaFa1726/antigravity-rtl-patcher/releases/tag/v1.0.0
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # Antigravity RTL Patcher
2
+
3
+ > RTL (Right-to-Left) support for [Antigravity](https://antigravity.dev) desktop app with Persian/Arabic text.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/antigravity-rtl-patcher)](https://www.npmjs.com/package/antigravity-rtl-patcher)
6
+ [![npm downloads](https://img.shields.io/npm/dt/antigravity-rtl-patcher)](https://www.npmjs.com/package/antigravity-rtl-patcher)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
+
9
+ ## ✨ Features
10
+
11
+ - 🌐 Smart RTL detection for Persian/Arabic text
12
+ - šŸ“ RTL support for messages, tables, and code blocks
13
+ - šŸŽØ UI elements (sidebar, navigation) stay LTR
14
+ - āŒØļø Toggle with `Alt + R` keyboard shortcut
15
+ - šŸ’¾ Settings saved automatically
16
+ - šŸ”¤ Proper character rendering with system fonts
17
+
18
+ ## šŸš€ Installation
19
+
20
+ ### Option 1: From npm (Recommended)
21
+
22
+ ```bash
23
+ npx antigravity-rtl-patcher@latest patch
24
+ ```
25
+
26
+ ### Option 2: Direct from GitHub
27
+
28
+ ```bash
29
+ npx github:VaFa1726/antigravity-rtl-patcher patch
30
+ ```
31
+
32
+ Then restart Antigravity and press `Alt + R` to toggle RTL mode.
33
+
34
+ ## šŸ“‹ Commands
35
+
36
+ | Command | Description |
37
+ |---------|-------------|
38
+ | `agy-rtl patch` | Apply RTL patch |
39
+ | `agy-rtl restore` | Restore original |
40
+ | `agy-rtl status` | Check patch status |
41
+ | `agy-rtl update` | Check for updates |
42
+
43
+ ## šŸ”§ Custom Installation Path
44
+
45
+ ```bash
46
+ npx antigravity-rtl-patcher patch --path /your/custom/path
47
+ ```
48
+
49
+ ## šŸ”„ After Antigravity Updates
50
+
51
+ Re-apply the patch after updating Antigravity:
52
+
53
+ **From npm:**
54
+ ```bash
55
+ npx antigravity-rtl-patcher@latest patch
56
+ ```
57
+
58
+ **From GitHub:**
59
+ ```bash
60
+ npx github:VaFa1726/antigravity-rtl-patcher patch
61
+ ```
62
+
63
+ > **šŸ’” Tip:** Use GitHub installation to get the latest changes immediately. npm version is updated every 24 hours.
64
+
65
+ ## šŸ› ļø Troubleshooting
66
+
67
+ **Restore original if needed:**
68
+
69
+ ```bash
70
+ npx antigravity-rtl-patcher restore
71
+ ```
72
+
73
+ ## šŸ“„ License
74
+
75
+ MIT Ā© [VaFa1726](https://github.com/VaFa1726)
76
+
77
+ ---
78
+
79
+ **Made with ā¤ļø for Persian/Arabic Developers**
package/bin/cli.js ADDED
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { program } = require('commander');
4
+ const chalk = require('chalk');
5
+ const figlet = require('figlet');
6
+ const { patch, restore, status } = require('../src/patcher');
7
+ const { checkForUpdates, showUpdateInstructions, currentVersion } = require('../src/version-checker');
8
+
9
+ /**
10
+ * Display colorful banner with gradient effect
11
+ */
12
+ function showBanner() {
13
+ try {
14
+ const bannerText = figlet.textSync('Antigravity RTL', {
15
+ font: 'Standard',
16
+ horizontalLayout: 'default',
17
+ verticalLayout: 'default'
18
+ });
19
+
20
+ // Gradient colors
21
+ const colors = [
22
+ { r: 51, g: 135, b: 255 }, // Blue
23
+ { r: 242, g: 80, b: 65 }, // Red
24
+ { r: 223, g: 172, b: 42 }, // Yellow
25
+ { r: 145, g: 196, b: 91 } // Green
26
+ ];
27
+
28
+ const lines = bannerText.split('\n');
29
+ console.log('');
30
+
31
+ lines.forEach((line, lineIndex) => {
32
+ if (!line.trim()) return;
33
+
34
+ let coloredLine = '';
35
+ const chars = line.split('');
36
+ const totalChars = chars.filter(c => c !== ' ').length;
37
+ let charIndex = 0;
38
+
39
+ chars.forEach(char => {
40
+ if (char === ' ') {
41
+ coloredLine += ' ';
42
+ } else {
43
+ const progress = totalChars > 1 ? charIndex / (totalChars - 1) : 0;
44
+ const segmentCount = colors.length - 1;
45
+ const segmentFloat = progress * segmentCount;
46
+ const segmentIndex = Math.min(Math.floor(segmentFloat), segmentCount - 1);
47
+ const segmentProgress = segmentFloat - segmentIndex;
48
+
49
+ const c1 = colors[segmentIndex];
50
+ const c2 = colors[segmentIndex + 1];
51
+
52
+ const r = Math.round(c1.r + segmentProgress * (c2.r - c1.r));
53
+ const g = Math.round(c1.g + segmentProgress * (c2.g - c1.g));
54
+ const b = Math.round(c1.b + segmentProgress * (c2.b - c1.b));
55
+
56
+ coloredLine += `\x1b[38;2;${r};${g};${b}m${char}\x1b[0m`;
57
+ charIndex++;
58
+ }
59
+ });
60
+
61
+ console.log(coloredLine);
62
+ });
63
+
64
+ console.log('');
65
+ console.log(chalk.gray(` RTL Support for Persian/Arabic Text | v${currentVersion}\n`));
66
+ } catch (err) {
67
+ // Fallback banner
68
+ console.log(chalk.cyan.bold(`\n✨ Antigravity Smart RTL Patcher v${currentVersion}\n`));
69
+ }
70
+ }
71
+
72
+ // Show banner
73
+ showBanner();
74
+
75
+ // CLI setup
76
+ program
77
+ .name('agy-rtl')
78
+ .description('RTL patcher for Antigravity')
79
+ .version(currentVersion);
80
+
81
+ program
82
+ .command('patch')
83
+ .description('Apply RTL patch to Antigravity')
84
+ .option('-p, --path <path>', 'Custom path to Antigravity installation')
85
+ .option('--skip-update-check', 'Skip checking for updates')
86
+ .action(async (options) => {
87
+ try {
88
+ await patch(options.path, options.skipUpdateCheck);
89
+ } catch (err) {
90
+ console.error(chalk.red('\nāœ– Patch failed:'), err.message);
91
+ process.exit(1);
92
+ }
93
+ });
94
+
95
+ program
96
+ .command('restore')
97
+ .description('Restore original Antigravity (remove patch)')
98
+ .option('-p, --path <path>', 'Custom path to Antigravity installation')
99
+ .action(async (options) => {
100
+ try {
101
+ await restore(options.path);
102
+ } catch (err) {
103
+ console.error(chalk.red('\nāœ– Restore failed:'), err.message);
104
+ process.exit(1);
105
+ }
106
+ });
107
+
108
+ program
109
+ .command('status')
110
+ .description('Check current patch status')
111
+ .option('-p, --path <path>', 'Custom path to Antigravity installation')
112
+ .action(async (options) => {
113
+ try {
114
+ await status(options.path);
115
+ } catch (err) {
116
+ console.error(chalk.red('\nāœ– Status check failed:'), err.message);
117
+ process.exit(1);
118
+ }
119
+ });
120
+
121
+ program
122
+ .command('update')
123
+ .description('Check for updates and show installation instructions')
124
+ .action(async () => {
125
+ console.log(chalk.cyan('Checking for updates...\n'));
126
+ const hasUpdate = await checkForUpdates(false);
127
+ if (hasUpdate) {
128
+ showUpdateInstructions();
129
+ }
130
+ });
131
+
132
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "antigravity-rtl-patch",
3
+ "version": "1.0.2",
4
+ "description": "Advanced RTL and Persian/Arabic typography patcher for Antigravity with customizable UI",
5
+ "main": "src/patcher.js",
6
+ "bin": {
7
+ "antigravity-rtl-patcher": "./bin/cli.js",
8
+ "agy-rtl": "./bin/cli.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node ./bin/cli.js",
12
+ "patch": "node ./bin/cli.js patch",
13
+ "restore": "node ./bin/cli.js restore"
14
+ },
15
+ "keywords": [
16
+ "antigravity",
17
+ "rtl",
18
+ "farsi",
19
+ "persian",
20
+ "arabic",
21
+ "hebrew",
22
+ "patcher",
23
+ "electron",
24
+ "asar",
25
+ "right-to-left",
26
+ "bidirectional",
27
+ "ui-customization",
28
+ "font-control"
29
+ ],
30
+ "author": "VaFa1726",
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
+ },
43
+ "dependencies": {
44
+ "@electron/asar": "^3.2.14",
45
+ "chalk": "^4.1.2",
46
+ "commander": "^11.1.0",
47
+ "figlet": "^1.7.0",
48
+ "fs-extra": "^11.2.0",
49
+ "ora": "^5.4.1",
50
+ "picocolors": "^1.0.0",
51
+ "prompts": "^2.4.2"
52
+ }
53
+ }
@@ -0,0 +1,409 @@
1
+ /* ANTIGRAVITY_RTL_PATCH_v3 */
2
+
3
+ win.webContents.on('console-message', (event, ...args) => {
4
+ let message = '';
5
+ if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) {
6
+ message = args[0].message;
7
+ } else {
8
+ message = args[1];
9
+ }
10
+
11
+ if (typeof message === 'string' && message.startsWith('RTL_CONFIG_SAVE:')) {
12
+ try {
13
+ const configData = message.substring(16);
14
+ const configPath = require('path').join(require('os').homedir(), '.antigravity-rtl.json');
15
+ require('fs').writeFileSync(configPath, configData, 'utf8');
16
+ } catch (err) {}
17
+ }
18
+ });
19
+
20
+ void win.loadURL(url);
21
+
22
+ win.webContents.on('dom-ready', () => {
23
+ try {
24
+ let rtlConfig = { enabled: true };
25
+ try {
26
+ const configPath = require('path').join(require('os').homedir(), '.antigravity-rtl.json');
27
+ if (require('fs').existsSync(configPath)) {
28
+ const cfg = JSON.parse(require('fs').readFileSync(configPath, 'utf8'));
29
+ rtlConfig = { ...rtlConfig, ...cfg };
30
+ }
31
+ } catch (err) {}
32
+
33
+ win.webContents.executeJavaScript(`
34
+ (function initRTL() {
35
+ if (window.__RTL_LOADED__) return;
36
+ window.__RTL_LOADED__ = true;
37
+
38
+ let rtlEnabled = ${rtlConfig.enabled};
39
+ let panelVisible = false;
40
+ let mutObserver = null;
41
+
42
+ // Persian / Arabic / Hebrew unicode ranges
43
+ const RTL_REGEX = /[\\u0600-\\u06FF\\u0750-\\u077F\\u08A0-\\u08FF\\uFB50-\\uFDFD\\uFE70-\\uFEFF\\u0590-\\u05FF]/;
44
+
45
+ // Block-level elements where we want to set direction
46
+ const BLOCK_TAGS = new Set([
47
+ 'P','LI','H1','H2','H3','H4','H5','H6',
48
+ 'TD','TH','BLOCKQUOTE','DIV','SECTION','ARTICLE','SPAN'
49
+ ]);
50
+
51
+ // Tags whose text should never be RTL
52
+ const ALWAYS_LTR_TAGS = new Set([
53
+ 'PRE','CODE','SCRIPT','STYLE','BUTTON','INPUT',
54
+ 'SELECT','OPTION','LABEL','SVG','PATH','NAV',
55
+ 'ASIDE','HEADER','FOOTER'
56
+ ]);
57
+
58
+ // Ancestor class patterns → skip RTL (ONLY very specific UI areas)
59
+ const SKIP_PATTERNS = [
60
+ 'sidebar', 'nav-', '-nav', 'navigation',
61
+ 'toolbar', 'topbar',
62
+ 'conversation-list', 'history-list',
63
+ 'project-list', 'settings-page',
64
+ 'rtl-ui'
65
+ ];
66
+
67
+ function isInsideLTRArea(el) {
68
+ let node = el;
69
+ let depth = 0;
70
+ while (node && depth < 15) {
71
+ if (!node.tagName) { node = node.parentElement; depth++; continue; }
72
+
73
+ // Always-LTR tags
74
+ if (ALWAYS_LTR_TAGS.has(node.tagName)) return true;
75
+
76
+ // RTL panel itself
77
+ if (node.id === 'rtl-panel' || node.id === 'rtl-trigger' || node.id === 'rtl-toggle') return true;
78
+
79
+ // Semantic nav/aside/header/footer
80
+ if (['NAV','ASIDE','HEADER','FOOTER'].includes(node.tagName)) return true;
81
+
82
+ // Role-based skip
83
+ const role = node.getAttribute ? (node.getAttribute('role') || '') : '';
84
+ if (['navigation','menubar','menu','tablist','complementary'].includes(role)) return true;
85
+
86
+ // Class-based skip — ONLY very specific patterns
87
+ if (node.classList && node.classList.length) {
88
+ const classStr = Array.from(node.classList).join(' ').toLowerCase();
89
+ for (const pat of SKIP_PATTERNS) {
90
+ if (classStr.includes(pat)) return true;
91
+ }
92
+ }
93
+
94
+ node = node.parentElement;
95
+ depth++;
96
+ }
97
+ return false;
98
+ }
99
+
100
+ // Find nearest block-level ancestor to set direction on
101
+ function getNearestBlock(textNode) {
102
+ let el = textNode.parentElement;
103
+ while (el) {
104
+ if (BLOCK_TAGS.has(el.tagName)) return el;
105
+ el = el.parentElement;
106
+ }
107
+ return textNode.parentElement;
108
+ }
109
+
110
+ // Walk all text nodes under root, find those with RTL chars,
111
+ // and mark their nearest block container as RTL
112
+ function applyRTLToSubtree(root) {
113
+ const walker = document.createTreeWalker(
114
+ root,
115
+ NodeFilter.SHOW_TEXT,
116
+ {
117
+ acceptNode(node) {
118
+ // Skip empty text nodes
119
+ if (!node.textContent.trim()) return NodeFilter.FILTER_SKIP;
120
+ // Skip if parent is a "always LTR" tag
121
+ if (node.parentElement && ALWAYS_LTR_TAGS.has(node.parentElement.tagName)) return NodeFilter.FILTER_SKIP;
122
+ return NodeFilter.FILTER_ACCEPT;
123
+ }
124
+ }
125
+ );
126
+
127
+ const seen = new Set();
128
+ let textNode;
129
+ while ((textNode = walker.nextNode())) {
130
+ if (!RTL_REGEX.test(textNode.textContent)) continue;
131
+
132
+ const block = getNearestBlock(textNode);
133
+ if (!block || seen.has(block)) continue;
134
+ seen.add(block);
135
+
136
+ if (isInsideLTRArea(block)) continue;
137
+
138
+ block.setAttribute('data-rtl-forced', '1');
139
+ block.style.direction = 'rtl';
140
+ block.style.textAlign = 'right';
141
+ }
142
+ }
143
+
144
+ function removeRTLFromAll() {
145
+ document.querySelectorAll('[data-rtl-forced]').forEach(el => {
146
+ el.style.direction = '';
147
+ el.style.textAlign = '';
148
+ el.removeAttribute('data-rtl-forced');
149
+ });
150
+ }
151
+
152
+ // ─── CSS: only what JS can't handle ───────────────────────
153
+ const CSS = \`
154
+ /* Code blocks: always LTR */
155
+ body.rtl-active pre,
156
+ body.rtl-active code,
157
+ body.rtl-active pre *,
158
+ body.rtl-active code * {
159
+ direction: ltr !important;
160
+ text-align: left !important;
161
+ font-family: 'Courier New', Consolas, Monaco, monospace !important;
162
+ }
163
+ /* NOTE: textarea/input RTL is handled by JS (setupInputRTL)
164
+ so placeholder stays LTR when field is empty */
165
+ /* Lists padding */
166
+ body.rtl-active [data-rtl-forced] ul,
167
+ body.rtl-active [data-rtl-forced] ol {
168
+ padding-left: 0 !important;
169
+ padding-right: 1.5rem !important;
170
+ }
171
+ /* Panel UI */
172
+ #rtl-trigger {
173
+ position: fixed; bottom: 20px; right: 20px; z-index: 999998;
174
+ width: 50px; height: 50px; border-radius: 50%;
175
+ background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
176
+ border: none; color: white; cursor: pointer;
177
+ display: flex; align-items: center; justify-content: center;
178
+ font-size: 24px; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
179
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
180
+ }
181
+ #rtl-trigger:hover {
182
+ transform: scale(1.1) rotate(180deg);
183
+ background: linear-gradient(135deg, #3182ce 0%, #2c5282 100%);
184
+ box-shadow: 0 6px 20px rgba(49,130,206,0.4);
185
+ }
186
+ #rtl-trigger.active {
187
+ background: linear-gradient(135deg, #3182ce 0%, #2c5282 100%);
188
+ box-shadow: 0 4px 12px rgba(49,130,206,0.4);
189
+ }
190
+ #rtl-panel {
191
+ position: fixed; bottom: 80px; right: 20px; z-index: 999999;
192
+ width: 300px; background: rgba(255,255,255,0.95);
193
+ backdrop-filter: blur(10px); border-radius: 16px;
194
+ box-shadow: 0 8px 32px rgba(0,0,0,0.2);
195
+ opacity: 0; transform: translateY(20px) scale(0.9);
196
+ pointer-events: none;
197
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
198
+ overflow: hidden; direction: ltr;
199
+ }
200
+ #rtl-panel.visible { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; }
201
+ @media (prefers-color-scheme: dark) {
202
+ #rtl-panel { background: rgba(30,30,40,0.95); color: #e0e0e0; }
203
+ }
204
+ .rtl-panel-header {
205
+ padding: 20px;
206
+ background: linear-gradient(135deg, #3182ce 0%, #2c5282 100%);
207
+ color: white; text-align: center; font-weight: 600;
208
+ font-size: 16px; direction: ltr;
209
+ }
210
+ .rtl-panel-body { padding: 24px; direction: ltr; }
211
+ .rtl-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
212
+ .rtl-label { font-size: 14px; font-weight: 500; color: #333; }
213
+ @media (prefers-color-scheme: dark) { .rtl-label { color: #e0e0e0; } }
214
+ .rtl-switch {
215
+ position: relative; width: 52px; height: 28px;
216
+ background: #ddd; border-radius: 14px;
217
+ cursor: pointer; transition: background 0.3s;
218
+ }
219
+ .rtl-switch.active { background: linear-gradient(135deg, #3182ce 0%, #2c5282 100%); }
220
+ .rtl-switch::after {
221
+ content: ''; position: absolute;
222
+ width: 22px; height: 22px; background: white;
223
+ border-radius: 50%; top: 3px; left: 3px;
224
+ transition: transform 0.3s; box-shadow: 0 2px 4px rgba(0,0,0,0.2);
225
+ }
226
+ .rtl-switch.active::after { transform: translateX(24px); }
227
+ .rtl-panel-footer {
228
+ padding: 16px 24px;
229
+ border-top: 1px solid rgba(0,0,0,0.1);
230
+ display: flex; justify-content: center; direction: ltr;
231
+ }
232
+ @media (prefers-color-scheme: dark) {
233
+ .rtl-panel-footer { border-top: 1px solid rgba(255,255,255,0.1); }
234
+ }
235
+ .rtl-github-btn {
236
+ display: flex; align-items: center; gap: 8px;
237
+ padding: 10px 20px; background: #24292e; color: white;
238
+ border: none; border-radius: 8px; cursor: pointer;
239
+ font-size: 13px; font-weight: 500; text-decoration: none;
240
+ transition: all 0.2s; direction: ltr;
241
+ }
242
+ .rtl-github-btn:hover {
243
+ background: #1a1f23; transform: translateY(-2px);
244
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2);
245
+ }
246
+ .rtl-github-icon { width: 18px; height: 18px; }
247
+ \`;
248
+
249
+ const styleEl = document.createElement('style');
250
+ styleEl.id = 'rtl-main-style';
251
+ styleEl.textContent = CSS;
252
+ document.head.appendChild(styleEl);
253
+
254
+ if (rtlEnabled) {
255
+ document.body.classList.add('rtl-active');
256
+ applyRTLToSubtree(document.body);
257
+ }
258
+
259
+ // ─── MutationObserver: handle new messages (SPA) ──────────
260
+ function startObserver() {
261
+ if (mutObserver) return;
262
+ mutObserver = new MutationObserver((mutations) => {
263
+ if (!rtlEnabled) return;
264
+ mutations.forEach(m => {
265
+ m.addedNodes.forEach(n => {
266
+ if (n.nodeType === 1) {
267
+ applyRTLToSubtree(n);
268
+ if (n.matches && n.matches('textarea, [contenteditable="true"], [role="textbox"]')) {
269
+ setupInputRTL(n);
270
+ }
271
+ n.querySelectorAll && n.querySelectorAll('textarea, [contenteditable="true"], [role="textbox"]').forEach(setupInputRTL);
272
+ }
273
+ });
274
+ // Re-check parent when text content changes
275
+ if (m.type === 'characterData' && m.target.parentElement) {
276
+ applyRTLToSubtree(m.target.parentElement);
277
+ }
278
+ });
279
+ });
280
+ mutObserver.observe(document.body, {
281
+ childList: true, subtree: true, characterData: true
282
+ });
283
+ }
284
+
285
+ function stopObserver() {
286
+ if (mutObserver) { mutObserver.disconnect(); mutObserver = null; }
287
+ }
288
+
289
+ if (rtlEnabled) startObserver();
290
+
291
+ // ─── Smart input RTL ──────────────────────────────────────
292
+ // Apply RTL to text inputs ONLY when user has typed RTL chars.
293
+ // When field is empty, direction is reset so placeholder stays LTR.
294
+ function updateInputDir(el) {
295
+ const text = el.value !== undefined ? el.value : (el.innerText || '');
296
+ if (!text.trim()) {
297
+ // Empty — reset so placeholder shows correctly
298
+ el.style.direction = '';
299
+ el.style.textAlign = '';
300
+ } else if (RTL_REGEX.test(text)) {
301
+ el.style.direction = 'rtl';
302
+ el.style.textAlign = 'right';
303
+ } else {
304
+ // LTR content — also reset
305
+ el.style.direction = '';
306
+ el.style.textAlign = '';
307
+ }
308
+ }
309
+
310
+ function setupInputRTL(el) {
311
+ if (!el || el.dataset.rtlInputBound) return;
312
+ el.dataset.rtlInputBound = '1';
313
+ el.addEventListener('input', () => { if (rtlEnabled) updateInputDir(el); });
314
+ el.addEventListener('keyup', () => { if (rtlEnabled) updateInputDir(el); });
315
+ if (rtlEnabled) updateInputDir(el);
316
+ }
317
+
318
+ function setupAllInputs() {
319
+ document.querySelectorAll(
320
+ 'textarea, [contenteditable="true"], [role="textbox"]'
321
+ ).forEach(setupInputRTL);
322
+ }
323
+
324
+ function teardownAllInputs() {
325
+ document.querySelectorAll('[data-rtl-input-bound]').forEach(el => {
326
+ el.style.direction = '';
327
+ el.style.textAlign = '';
328
+ });
329
+ }
330
+
331
+ if (rtlEnabled) setupAllInputs();
332
+
333
+ // ─── UI ───────────────────────────────────────────────────
334
+ const trigger = document.createElement('button');
335
+ trigger.id = 'rtl-trigger';
336
+ trigger.className = rtlEnabled ? 'active rtl-ui' : 'rtl-ui';
337
+ trigger.innerHTML = '⇄';
338
+ trigger.title = 'RTL Settings (Alt+R)';
339
+
340
+ const panel = document.createElement('div');
341
+ panel.id = 'rtl-panel';
342
+ panel.className = 'rtl-ui';
343
+ panel.innerHTML = \`
344
+ <div class="rtl-panel-header rtl-ui">Antigravity Smart RTL</div>
345
+ <div class="rtl-panel-body rtl-ui">
346
+ <div class="rtl-row rtl-ui">
347
+ <span class="rtl-label rtl-ui">RTL Mode</span>
348
+ <div class="rtl-switch \${rtlEnabled ? 'active' : ''} rtl-ui" id="rtl-toggle"></div>
349
+ </div>
350
+ </div>
351
+ <div class="rtl-panel-footer rtl-ui">
352
+ <a href="https://github.com/VaFa1726/antigravity-rtl-patcher" target="_blank" class="rtl-github-btn rtl-ui">
353
+ <svg class="rtl-github-icon rtl-ui" viewBox="0 0 16 16" fill="currentColor">
354
+ <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
355
+ </svg>
356
+ Star on GitHub
357
+ </a>
358
+ </div>
359
+ \`;
360
+
361
+ document.body.appendChild(trigger);
362
+ document.body.appendChild(panel);
363
+
364
+ trigger.addEventListener('click', (e) => {
365
+ e.stopPropagation();
366
+ panelVisible = !panelVisible;
367
+ panel.classList.toggle('visible', panelVisible);
368
+ });
369
+
370
+ document.addEventListener('click', (e) => {
371
+ if (!e.target.classList.contains('rtl-ui')) {
372
+ panelVisible = false;
373
+ panel.classList.remove('visible');
374
+ }
375
+ });
376
+
377
+ document.getElementById('rtl-toggle').addEventListener('click', () => {
378
+ rtlEnabled = !rtlEnabled;
379
+ document.getElementById('rtl-toggle').classList.toggle('active', rtlEnabled);
380
+ trigger.classList.toggle('active', rtlEnabled);
381
+
382
+ if (rtlEnabled) {
383
+ document.body.classList.add('rtl-active');
384
+ applyRTLToSubtree(document.body);
385
+ setupAllInputs();
386
+ startObserver();
387
+ } else {
388
+ document.body.classList.remove('rtl-active');
389
+ stopObserver();
390
+ removeRTLFromAll();
391
+ teardownAllInputs();
392
+ }
393
+
394
+ console.log('RTL_CONFIG_SAVE:' + JSON.stringify({ enabled: rtlEnabled }));
395
+ });
396
+
397
+ document.addEventListener('keydown', (e) => {
398
+ if (e.altKey && e.code === 'KeyR') {
399
+ e.preventDefault();
400
+ document.getElementById('rtl-toggle').click();
401
+ }
402
+ });
403
+ })();
404
+ `).catch(err => console.error('[RTL] Injection failed:', err));
405
+
406
+ } catch (error) {
407
+ console.error('[RTL] Init error:', error);
408
+ }
409
+ });
package/src/patcher.js ADDED
@@ -0,0 +1,240 @@
1
+ const asar = require('@electron/asar');
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const ora = require('ora');
6
+ const chalk = require('chalk');
7
+ const prompts = require('prompts');
8
+ const { findInstallations, findUtilsJs, detectInstallation } = require('./paths');
9
+ const { checkPermissions, delay } = require('./utils');
10
+ const { checkForUpdates } = require('./version-checker');
11
+
12
+ const PATCH_MARKER = '/* ANTIGRAVITY_RTL_PATCH_v3 */';
13
+ const BACKUP_SUFFIX = '.agy-rtl-backup';
14
+ const INJECTION_ANCHOR = 'void win.loadURL(url);';
15
+
16
+ /**
17
+ * Check if an ASAR is already patched by looking for the marker
18
+ */
19
+ async function isAlreadyPatched(extractDir) {
20
+ const utilsPath = path.join(extractDir, 'dist', 'utils.js');
21
+
22
+ if (!fs.existsSync(utilsPath)) {
23
+ return false;
24
+ }
25
+
26
+ const content = await fs.readFile(utilsPath, 'utf-8');
27
+ return content.includes(PATCH_MARKER);
28
+ }
29
+
30
+ /**
31
+ * Patch an ASAR-packed Antigravity installation
32
+ */
33
+ async function patchAsar(installation, spinner) {
34
+ const { asarPath } = installation;
35
+ const backupPath = asarPath + BACKUP_SUFFIX;
36
+ const tmpDir = path.join(os.tmpdir(), 'agy-rtl-' + Date.now());
37
+
38
+ try {
39
+ // 1. Backup
40
+ if (!fs.existsSync(backupPath)) {
41
+ spinner.text = 'Creating backup...';
42
+ await fs.copy(asarPath, backupPath);
43
+ spinner.succeed('Backup created');
44
+ } else {
45
+ spinner.info('Backup already exists, skipping');
46
+ }
47
+
48
+ // 2. Extract
49
+ spinner.start('Extracting app.asar...');
50
+ await fs.ensureDir(tmpDir);
51
+ asar.extractAll(asarPath, tmpDir);
52
+ spinner.succeed('Extracted successfully');
53
+
54
+ // 3. Check if already patched
55
+ if (await isAlreadyPatched(tmpDir)) {
56
+ spinner.info('Already patched with latest version!');
57
+ await fs.remove(tmpDir);
58
+ return;
59
+ }
60
+
61
+ // 4. Find utils.js
62
+ spinner.start('Locating utils.js...');
63
+ const utilsPath = findUtilsJs(tmpDir);
64
+
65
+ if (!utilsPath) {
66
+ spinner.warn('utils.js not found - unsupported Antigravity version');
67
+ await fs.remove(tmpDir);
68
+ return;
69
+ }
70
+
71
+ spinner.succeed('Found: ' + chalk.gray(path.relative(tmpDir, utilsPath)));
72
+
73
+ // 5. Read utils.js content
74
+ spinner.start('Injecting RTL engine...');
75
+ let utilsContent = await fs.readFile(utilsPath, 'utf-8');
76
+
77
+ // 6. Check for injection anchor
78
+ if (!utilsContent.includes(INJECTION_ANCHOR)) {
79
+ spinner.warn('Injection anchor not found - this Antigravity version may not be supported');
80
+ await fs.remove(tmpDir);
81
+ return;
82
+ }
83
+
84
+ // 7. Read injection payload
85
+ const payloadPath = path.join(__dirname, '..', 'payload', 'utils-inject.js');
86
+ const payload = await fs.readFile(payloadPath, 'utf-8');
87
+
88
+ // 8. Inject payload (replace the anchor line with payload)
89
+ utilsContent = utilsContent.replace(INJECTION_ANCHOR, payload);
90
+
91
+ // 9. Force enable DevTools for debugging
92
+ utilsContent = utilsContent.replace(
93
+ /devTools:\s*!electron_1?\.app\.isPackaged/g,
94
+ 'devTools: true'
95
+ );
96
+
97
+ // 10. Write modified utils.js
98
+ await fs.writeFile(utilsPath, utilsContent, 'utf-8');
99
+
100
+ spinner.succeed('RTL engine injected successfully');
101
+
102
+ // 12. Repack
103
+ spinner.start('Repacking app.asar...');
104
+ await delay(300);
105
+ await asar.createPackage(tmpDir, asarPath);
106
+ spinner.succeed('Repacked successfully');
107
+
108
+ } finally {
109
+ // Cleanup
110
+ if (fs.existsSync(tmpDir)) {
111
+ await fs.remove(tmpDir);
112
+ }
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Restore an ASAR installation from backup
118
+ */
119
+ async function restoreAsar(installation, spinner) {
120
+ const { asarPath } = installation;
121
+ const backupPath = asarPath + BACKUP_SUFFIX;
122
+
123
+ if (fs.existsSync(backupPath)) {
124
+ spinner.start('Restoring from backup...');
125
+ await fs.copy(backupPath, asarPath);
126
+ await fs.remove(backupPath);
127
+ spinner.succeed('Original app.asar restored');
128
+ } else {
129
+ spinner.warn('No backup found');
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Main patch function
135
+ */
136
+ async function patch(customPath, skipUpdateCheck = false) {
137
+ // Check for updates
138
+ if (!skipUpdateCheck) {
139
+ await checkForUpdates(false);
140
+ }
141
+
142
+ const spinner = ora('Searching for Antigravity...').start();
143
+ const installations = findInstallations(customPath);
144
+
145
+ if (installations.length === 0) {
146
+ spinner.fail('Antigravity installation not found in common paths');
147
+ spinner.stop();
148
+
149
+ // Ask user to enter path manually
150
+ const response = await prompts({
151
+ type: 'text',
152
+ name: 'manualPath',
153
+ message: chalk.yellow('Enter the path to your Antigravity installation folder:'),
154
+ validate: (val) => {
155
+ if (!val || !val.trim()) return 'Path cannot be empty';
156
+ if (!fs.existsSync(val.trim())) return 'Path does not exist';
157
+ const info = detectInstallation(val.trim());
158
+ if (!info) return 'No app.asar found at that path (expected: <path>/resources/app.asar)';
159
+ return true;
160
+ }
161
+ });
162
+
163
+ if (!response.manualPath) {
164
+ throw new Error('No path provided — patch cancelled');
165
+ }
166
+
167
+ const info = detectInstallation(response.manualPath.trim());
168
+ installations.push(info);
169
+ }
170
+
171
+ spinner.succeed('Found ' + installations.length + ' installation(s)');
172
+
173
+ for (const inst of installations) {
174
+ console.log(chalk.cyan('\n Patching: ') + chalk.white(inst.basePath));
175
+ checkPermissions(inst.basePath);
176
+ await patchAsar(inst, ora());
177
+ }
178
+
179
+ console.log(chalk.green.bold('\n ✨ Antigravity patched successfully!'));
180
+ console.log(chalk.cyan(' Restart Antigravity and press Alt + R to toggle RTL mode.\n'));
181
+ }
182
+
183
+ /**
184
+ * Main restore function
185
+ */
186
+ async function restore(customPath) {
187
+ const spinner = ora('Searching for Antigravity...').start();
188
+ const installations = findInstallations(customPath);
189
+
190
+ if (installations.length === 0) {
191
+ spinner.fail('Antigravity installation not found');
192
+ throw new Error('Installation not found');
193
+ }
194
+
195
+ spinner.succeed('Found ' + installations.length + ' installation(s)');
196
+
197
+ for (const inst of installations) {
198
+ console.log(chalk.yellow('\n Restoring: ') + chalk.white(inst.basePath));
199
+ checkPermissions(inst.basePath);
200
+ await restoreAsar(inst, ora());
201
+ }
202
+
203
+ console.log(chalk.green.bold('\n ✨ Antigravity restored to original state'));
204
+ console.log(chalk.cyan(' Restart Antigravity for changes to take effect.\n'));
205
+ }
206
+
207
+ /**
208
+ * Check patch status
209
+ */
210
+ async function status(customPath) {
211
+ const spinner = ora('Searching for Antigravity...').start();
212
+ const installations = findInstallations(customPath);
213
+
214
+ if (installations.length === 0) {
215
+ spinner.fail('Antigravity installation not found');
216
+ return;
217
+ }
218
+
219
+ spinner.succeed('Found ' + installations.length + ' installation(s)');
220
+
221
+ for (const inst of installations) {
222
+ const backupExists = fs.existsSync(inst.asarPath + BACKUP_SUFFIX);
223
+ const statusIcon = backupExists
224
+ ? chalk.green('āœ“ PATCHED')
225
+ : chalk.red('āœ— NOT PATCHED');
226
+
227
+ console.log('\n ' + statusIcon + ' ' + chalk.white(inst.basePath));
228
+
229
+ if (backupExists) {
230
+ console.log(chalk.gray(' Backup: ' + inst.asarPath + BACKUP_SUFFIX));
231
+ }
232
+ }
233
+ console.log('');
234
+ }
235
+
236
+ module.exports = {
237
+ patch,
238
+ restore,
239
+ status,
240
+ };
package/src/paths.js ADDED
@@ -0,0 +1,157 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ /**
6
+ * Resolve the real user home directory.
7
+ * When running under sudo, os.homedir() returns /root.
8
+ * We use SUDO_USER to find the actual user's home.
9
+ */
10
+ function resolveHome() {
11
+ let home = os.homedir();
12
+ if (process.env.SUDO_USER) {
13
+ const sudoHome = process.platform === 'darwin'
14
+ ? path.join('/Users', process.env.SUDO_USER)
15
+ : path.join('/home', process.env.SUDO_USER);
16
+ if (fs.existsSync(sudoHome)) {
17
+ home = sudoHome;
18
+ }
19
+ }
20
+ return home;
21
+ }
22
+
23
+ /**
24
+ * Known installation paths for Antigravity across platforms.
25
+ * Only targets the Antigravity desktop app (not the IDE).
26
+ */
27
+ function getSearchPaths() {
28
+ const home = resolveHome();
29
+ const platform = os.platform();
30
+
31
+ const common = [
32
+ path.join(home, 'Downloads', 'Antigravity'),
33
+ path.join(home, 'Downloads', 'Antigravity-x64'),
34
+ path.join(home, 'Desktop', 'Antigravity'),
35
+ path.join(home, 'Desktop', 'Antigravity-x64'),
36
+ ];
37
+
38
+ const platformPaths = {
39
+ linux: [
40
+ '/opt/antigravity',
41
+ '/opt/Antigravity',
42
+ '/usr/lib/antigravity',
43
+ '/usr/share/antigravity',
44
+ path.join(home, '.local', 'share', 'antigravity'),
45
+ path.join(home, '.local', 'lib', 'antigravity'),
46
+ ...common,
47
+ ],
48
+ darwin: [
49
+ '/Applications/Antigravity.app/Contents',
50
+ path.join(home, 'Applications', 'Antigravity.app', 'Contents'),
51
+ ...common,
52
+ ],
53
+ win32: [
54
+ path.join(process.env.LOCALAPPDATA || '', 'Programs', 'antigravity'),
55
+ path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Antigravity'),
56
+ path.join(process.env.PROGRAMFILES || '', 'Antigravity'),
57
+ ...common,
58
+ ],
59
+ };
60
+
61
+ return platformPaths[platform] || common;
62
+ }
63
+
64
+ /**
65
+ * Detect an Antigravity installation at a given base path.
66
+ * Looks for an ASAR package at resources/app.asar.
67
+ */
68
+ function detectInstallation(basePath) {
69
+ const asarPath = path.join(basePath, 'resources', 'app.asar');
70
+ if (fs.existsSync(asarPath)) {
71
+ return {
72
+ type: 'asar',
73
+ basePath,
74
+ asarPath,
75
+ };
76
+ }
77
+ return null;
78
+ }
79
+
80
+ /**
81
+ * Find utils.js inside an extracted ASAR directory.
82
+ * Searches common locations and falls back to recursive search.
83
+ */
84
+ function findUtilsJs(extractDir) {
85
+ // Common locations
86
+ const candidates = [
87
+ path.join(extractDir, 'dist', 'utils.js'),
88
+ path.join(extractDir, 'out', 'utils.js'),
89
+ path.join(extractDir, 'app', 'utils.js'),
90
+ path.join(extractDir, 'src', 'utils.js'),
91
+ ];
92
+
93
+ for (const candidate of candidates) {
94
+ if (fs.existsSync(candidate)) {
95
+ return candidate;
96
+ }
97
+ }
98
+
99
+ // Recursive fallback - find any utils.js (skip node_modules)
100
+ function searchDir(dir, depth) {
101
+ if (depth > 5) return null;
102
+
103
+ try {
104
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
105
+
106
+ for (const entry of entries) {
107
+ if (entry.name === 'node_modules') continue;
108
+
109
+ const fullPath = path.join(dir, entry.name);
110
+
111
+ if (entry.isFile() && entry.name === 'utils.js') {
112
+ return fullPath;
113
+ }
114
+
115
+ if (entry.isDirectory()) {
116
+ const found = searchDir(fullPath, depth + 1);
117
+ if (found) return found;
118
+ }
119
+ }
120
+ } catch (e) {
121
+ // Ignore permission errors
122
+ }
123
+
124
+ return null;
125
+ }
126
+
127
+ return searchDir(extractDir, 0);
128
+ }
129
+
130
+ /**
131
+ * Find all Antigravity installations on the system.
132
+ */
133
+ function findInstallations(customPath) {
134
+ const results = [];
135
+
136
+ if (customPath) {
137
+ const info = detectInstallation(customPath);
138
+ if (info) results.push(info);
139
+ return results;
140
+ }
141
+
142
+ const searchPaths = getSearchPaths();
143
+ for (const sp of searchPaths) {
144
+ if (fs.existsSync(sp)) {
145
+ const info = detectInstallation(sp);
146
+ if (info) results.push(info);
147
+ }
148
+ }
149
+
150
+ return results;
151
+ }
152
+
153
+ module.exports = {
154
+ findInstallations,
155
+ detectInstallation,
156
+ findUtilsJs,
157
+ };
package/src/utils.js ADDED
@@ -0,0 +1,35 @@
1
+ const chalk = require('chalk');
2
+
3
+ /**
4
+ * Check if the current process has sufficient permissions
5
+ * to modify system-level application files.
6
+ */
7
+ function checkPermissions(targetPath) {
8
+ const isWindows = process.platform === 'win32';
9
+
10
+ if (!isWindows) {
11
+ const isRoot = process.getuid && process.getuid() === 0;
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) {
16
+ console.error(chalk.red.bold('\nāš ļø Permission Denied!'));
17
+ console.error(chalk.white('Modifying system applications requires administrator privileges.'));
18
+ console.error(chalk.white(`Please run with ${chalk.yellow('sudo')}:\n`));
19
+ console.error(chalk.cyan(' sudo npx antigravity-rtl-patcher patch\n'));
20
+ process.exit(1);
21
+ }
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Simple async delay helper.
27
+ */
28
+ function delay(ms) {
29
+ return new Promise((resolve) => setTimeout(resolve, ms));
30
+ }
31
+
32
+ module.exports = {
33
+ checkPermissions,
34
+ delay,
35
+ };
@@ -0,0 +1,103 @@
1
+ const https = require('https');
2
+ const chalk = require('chalk');
3
+ const { version: currentVersion } = require('../package.json');
4
+
5
+ /**
6
+ * Compare two semantic versions.
7
+ * Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal
8
+ */
9
+ function compareVersions(v1, v2) {
10
+ const parts1 = v1.split('.').map(Number);
11
+ const parts2 = v2.split('.').map(Number);
12
+
13
+ for (let i = 0; i < 3; i++) {
14
+ const p1 = parts1[i] || 0;
15
+ const p2 = parts2[i] || 0;
16
+ if (p1 > p2) return 1;
17
+ if (p1 < p2) return -1;
18
+ }
19
+ return 0;
20
+ }
21
+
22
+ /**
23
+ * Fetch the latest version from npm registry.
24
+ */
25
+ function fetchLatestVersion() {
26
+ return new Promise((resolve, reject) => {
27
+ const url = 'https://registry.npmjs.org/antigravity-rtl-patcher/latest';
28
+
29
+ https.get(url, { timeout: 3000 }, (res) => {
30
+ let data = '';
31
+
32
+ res.on('data', (chunk) => { data += chunk; });
33
+ res.on('end', () => {
34
+ try {
35
+ const pkg = JSON.parse(data);
36
+ resolve(pkg.version);
37
+ } catch (e) {
38
+ reject(new Error('Failed to parse npm response'));
39
+ }
40
+ });
41
+ }).on('error', (err) => {
42
+ reject(err);
43
+ }).on('timeout', () => {
44
+ reject(new Error('Request timeout'));
45
+ });
46
+ });
47
+ }
48
+
49
+ /**
50
+ * Check if a newer version is available and notify the user.
51
+ * Returns: true if update is available, false otherwise
52
+ */
53
+ async function checkForUpdates(silent = false) {
54
+ try {
55
+ const latestVersion = await fetchLatestVersion();
56
+
57
+ if (compareVersions(latestVersion, currentVersion) > 0) {
58
+ if (!silent) {
59
+ console.log('');
60
+ console.log(chalk.yellow.bold('āš ļø Update Available!'));
61
+ console.log(chalk.white(` Current: ${chalk.red(currentVersion)} → Latest: ${chalk.green(latestVersion)}`));
62
+ console.log('');
63
+ console.log(chalk.cyan(' Run one of these commands to update:'));
64
+ console.log(chalk.gray(' • npx antigravity-rtl-patcher@latest patch'));
65
+ console.log(chalk.gray(' • npm install -g antigravity-rtl-patcher@latest'));
66
+ console.log('');
67
+ }
68
+ return true;
69
+ }
70
+
71
+ if (!silent) {
72
+ console.log(chalk.green('āœ“ You are using the latest version.\n'));
73
+ }
74
+ return false;
75
+ } catch (error) {
76
+ // Silently fail if network is unavailable
77
+ return false;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Show update instructions.
83
+ */
84
+ function showUpdateInstructions() {
85
+ console.log('');
86
+ console.log(chalk.cyan.bold('Update Instructions:'));
87
+ console.log('');
88
+ console.log(chalk.white('To update to the latest version, run:'));
89
+ console.log('');
90
+ console.log(chalk.green(' npx antigravity-rtl-patcher@latest patch'));
91
+ console.log(chalk.gray(' (This will automatically download and use the latest version)\n'));
92
+ console.log(chalk.white('Or install globally:'));
93
+ console.log('');
94
+ console.log(chalk.green(' npm install -g antigravity-rtl-patcher@latest'));
95
+ console.log(chalk.gray(' (Then you can use: agy-rtl patch)\n'));
96
+ }
97
+
98
+ module.exports = {
99
+ checkForUpdates,
100
+ showUpdateInstructions,
101
+ compareVersions,
102
+ currentVersion,
103
+ };