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 +61 -0
- package/README.md +79 -0
- package/bin/cli.js +132 -0
- package/package.json +53 -0
- package/payload/utils-inject.js +409 -0
- package/src/patcher.js +240 -0
- package/src/paths.js +157 -0
- package/src/utils.js +35 -0
- package/src/version-checker.js +103 -0
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
|
+
[](https://www.npmjs.com/package/antigravity-rtl-patcher)
|
|
6
|
+
[](https://www.npmjs.com/package/antigravity-rtl-patcher)
|
|
7
|
+
[](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
|
+
};
|