antigravity-tc 1.1.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.
@@ -0,0 +1,115 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const AutoLaunch = require('auto-launch');
5
+ const { ensureAppDirs, LOGS_DIR } = require('./paths');
6
+
7
+ // ============================================================
8
+ // macOS: LaunchAgent native.
9
+ // auto-launch tidak cocok untuk app berbasis node CLI: plist yang
10
+ // dihasilnya hanya berisi [node, --hidden] tanpa path script, jadi
11
+ // tidak pernah benar-benar menjalankan app saat login. Karena itu
12
+ // di darwin kita tulis plist sendiri dengan command lengkap.
13
+ // ============================================================
14
+ const PLIST_LABEL = 'com.antigravity-tc.launcher';
15
+ const LAUNCH_AGENTS_DIR = path.join(os.homedir(), 'Library', 'LaunchAgents');
16
+ const PLIST_PATH = path.join(LAUNCH_AGENTS_DIR, `${PLIST_LABEL}.plist`);
17
+ // Plist rusak buatan versi lama (auto-launch, label "node").
18
+ const LEGACY_PLIST_PATH = path.join(LAUNCH_AGENTS_DIR, 'node.plist');
19
+
20
+ const IS_DARWIN = process.platform === 'darwin';
21
+
22
+ // Fallback untuk platform non-macOS.
23
+ const launcher = new AutoLaunch({
24
+ name: 'AntigravityTokenCollector',
25
+ path: process.execPath,
26
+ args: ['--hidden'],
27
+ isHidden: true,
28
+ mac: {
29
+ useLaunchAgent: true,
30
+ },
31
+ });
32
+
33
+ function escapeXml(value) {
34
+ return String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
35
+ }
36
+
37
+ function buildPlistContent() {
38
+ const entryScript = path.join(__dirname, '..', 'index.js');
39
+ const programArguments = [process.execPath, entryScript, '--hidden'];
40
+ const argumentsXml = programArguments
41
+ .map((argument) => ` <string>${escapeXml(argument)}</string>`)
42
+ .join('\n');
43
+ const logPath = path.join(LOGS_DIR, 'launcher.log');
44
+ return `<?xml version="1.0" encoding="UTF-8"?>
45
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
46
+ <plist version="1.0">
47
+ <dict>
48
+ <key>Label</key>
49
+ <string>${PLIST_LABEL}</string>
50
+ <key>ProgramArguments</key>
51
+ <array>
52
+ ${argumentsXml}
53
+ </array>
54
+ <key>RunAtLoad</key>
55
+ <true/>
56
+ <key>StandardOutPath</key>
57
+ <string>${escapeXml(logPath)}</string>
58
+ <key>StandardErrorPath</key>
59
+ <string>${escapeXml(logPath)}</string>
60
+ </dict>
61
+ </plist>`;
62
+ }
63
+
64
+ // Hapus plist rusak buatan versi lama kalau terdeteksi (label "node" +
65
+ // argumen --hidden adalah signature milik app ini).
66
+ function removeLegacyPlistIfPresent() {
67
+ try {
68
+ if (!fs.existsSync(LEGACY_PLIST_PATH)) return;
69
+ const content = fs.readFileSync(LEGACY_PLIST_PATH, 'utf-8');
70
+ if (content.includes('<string>node</string>') && content.includes('--hidden')) {
71
+ fs.unlinkSync(LEGACY_PLIST_PATH);
72
+ }
73
+ } catch (e) {}
74
+ }
75
+
76
+ async function enableAutoStart() {
77
+ if (!IS_DARWIN) {
78
+ await launcher.enable();
79
+ return true;
80
+ }
81
+ ensureAppDirs();
82
+ removeLegacyPlistIfPresent();
83
+ fs.mkdirSync(LAUNCH_AGENTS_DIR, { recursive: true });
84
+ fs.writeFileSync(PLIST_PATH, buildPlistContent(), 'utf-8');
85
+ return true;
86
+ }
87
+
88
+ async function disableAutoStart() {
89
+ if (!IS_DARWIN) {
90
+ await launcher.disable();
91
+ return false;
92
+ }
93
+ removeLegacyPlistIfPresent();
94
+ try {
95
+ if (fs.existsSync(PLIST_PATH)) fs.unlinkSync(PLIST_PATH);
96
+ } catch (e) {}
97
+ return false;
98
+ }
99
+
100
+ async function isAutoStartEnabled() {
101
+ if (!IS_DARWIN) {
102
+ return launcher.isEnabled();
103
+ }
104
+ try {
105
+ return fs.existsSync(PLIST_PATH);
106
+ } catch (e) {
107
+ return false;
108
+ }
109
+ }
110
+
111
+ module.exports = {
112
+ enableAutoStart,
113
+ disableAutoStart,
114
+ isAutoStartEnabled,
115
+ };
package/src/cli.js ADDED
@@ -0,0 +1,82 @@
1
+ const { exec } = require('child_process');
2
+ const { select } = require('@inquirer/prompts');
3
+ const { version } = require('../package.json');
4
+
5
+ function openBrowser(url) {
6
+ const platform = process.platform;
7
+ let cmd;
8
+
9
+ if (platform === 'darwin') {
10
+ cmd = `open "${url}"`;
11
+ } else if (platform === 'win32') {
12
+ cmd = `start "" "${url}"`;
13
+ } else {
14
+ cmd = `xdg-open "${url}"`;
15
+ }
16
+
17
+ return new Promise((resolve) => {
18
+ exec(cmd, { windowsHide: true }, (err) => {
19
+ if (err) {
20
+ console.log(`Open browser manually: ${url}`);
21
+ }
22
+ resolve();
23
+ });
24
+ });
25
+ }
26
+
27
+ function printHeader(appUrl) {
28
+ console.clear();
29
+ console.log('========================================');
30
+ console.log(` Antigravity Token Collector v${version || '0.0.0'}`);
31
+ console.log(` Server: ${appUrl}`);
32
+ console.log('========================================');
33
+ console.log('');
34
+ }
35
+ async function showMainMenu({ trayManager, appUrl, onHideToTray, onExit }) {
36
+ printHeader(appUrl);
37
+ let choice;
38
+
39
+ try {
40
+ choice = await select({
41
+ message: 'Menu:',
42
+ choices: [
43
+ { name: 'Open Dashboard', value: 'dashboard' },
44
+ { name: 'Hide to Tray (Background)', value: 'hide' },
45
+ { name: 'Exit', value: 'exit' },
46
+ ],
47
+ });
48
+ } catch (error) {
49
+ if (error && error.name === 'ExitPromptError') {
50
+ choice = 'exit';
51
+ } else {
52
+ throw error;
53
+ }
54
+ }
55
+
56
+ if (choice === 'dashboard') {
57
+ const dashboardUrl = appUrl || 'http://localhost:3456/dashboard';
58
+ await openBrowser(dashboardUrl);
59
+ return showMainMenu({ trayManager, appUrl, onHideToTray, onExit });
60
+ }
61
+
62
+ if (choice === 'hide') {
63
+ if (typeof onHideToTray === 'function') {
64
+ await onHideToTray();
65
+ return;
66
+ }
67
+ console.log('✓ Hidden to tray. Klik icon di menubar macOS.');
68
+ return;
69
+ }
70
+
71
+ if (typeof onExit === 'function') {
72
+ await onExit();
73
+ return;
74
+ }
75
+
76
+ if (trayManager && trayManager.tray) {
77
+ await trayManager.stop();
78
+ }
79
+ process.exit(0);
80
+ }
81
+
82
+ module.exports = { showMainMenu };
package/src/config.js ADDED
@@ -0,0 +1,23 @@
1
+ const ConfImport = require('conf');
2
+ const Conf = ConfImport.default || ConfImport;
3
+
4
+ let configInstance = null;
5
+
6
+ function getConfig() {
7
+ if (!configInstance) {
8
+ configInstance = new Conf({
9
+ projectName: 'antigravity-token-collector',
10
+ defaults: {
11
+ port: 3456,
12
+ autoStart: false,
13
+ nineRouterDbPath: null,
14
+ tunnelEnabled: false,
15
+ adminPassword: null,
16
+ },
17
+ });
18
+ }
19
+
20
+ return configInstance;
21
+ }
22
+
23
+ module.exports = { getConfig };
package/src/jwt.js ADDED
@@ -0,0 +1,37 @@
1
+ const crypto = require('crypto');
2
+ const jwt = require('jsonwebtoken');
3
+
4
+ const TOKEN_MAX_AGE_SECONDS = 7 * 24 * 60 * 60;
5
+
6
+ // Secret JWT dibuat random sekali per instalasi lalu disimpan di config,
7
+ // sehingga token buatan instalasi A tidak bisa dipakai di instalasi B.
8
+ function getJwtSecret(config) {
9
+ let secret = config.get('jwtSecret');
10
+ if (!secret || typeof secret !== 'string' || secret.length < 32) {
11
+ secret = crypto.randomBytes(48).toString('hex');
12
+ config.set('jwtSecret', secret);
13
+ }
14
+ return secret;
15
+ }
16
+
17
+ function signAdminToken(config) {
18
+ return jwt.sign({ role: 'admin' }, getJwtSecret(config), {
19
+ expiresIn: TOKEN_MAX_AGE_SECONDS,
20
+ });
21
+ }
22
+
23
+ function verifyAdminToken(config, token) {
24
+ if (!token || typeof token !== 'string') return false;
25
+ try {
26
+ const payload = jwt.verify(token, getJwtSecret(config));
27
+ return payload && payload.role === 'admin';
28
+ } catch (e) {
29
+ return false;
30
+ }
31
+ }
32
+
33
+ module.exports = {
34
+ TOKEN_MAX_AGE_SECONDS,
35
+ signAdminToken,
36
+ verifyAdminToken,
37
+ };
package/src/paths.js ADDED
@@ -0,0 +1,59 @@
1
+ const os = require('os');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ const APP_DIR = path.join(os.homedir(), '.antigravity-tc');
6
+ const LOGS_DIR = path.join(APP_DIR, 'logs');
7
+ const BIN_DIR = path.join(APP_DIR, 'bin');
8
+
9
+ // Default lokasi database 9Router mengikuti OS tempat app di-install:
10
+ // - Windows : %APPDATA%/9router/db/data.sqlite
11
+ // - Docker : /app/data/db/data.sqlite (mount volume untuk persist)
12
+ // - lainnya : ~/.9router/db/data.sqlite (macOS/Linux)
13
+ function getDefault9RouterDb() {
14
+ if (process.platform === 'win32') {
15
+ const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
16
+ return path.join(appData, '9router', 'db', 'data.sqlite');
17
+ }
18
+ try {
19
+ if (fs.existsSync('/.dockerenv')) {
20
+ return '/app/data/db/data.sqlite';
21
+ }
22
+ } catch (e) {}
23
+ return path.join(os.homedir(), '.9router', 'db', 'data.sqlite');
24
+ }
25
+
26
+ const DEFAULT_9ROUTER_DB = getDefault9RouterDb();
27
+
28
+ function ensureAppDirs() {
29
+ for (const dir of [APP_DIR, LOGS_DIR, BIN_DIR]) {
30
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
31
+ }
32
+ }
33
+
34
+ function migrateLegacyLogDb(projectRoot) {
35
+ const targetPath = path.join(LOGS_DIR, 'donation-logs.sqlite');
36
+ if (fs.existsSync(targetPath)) return;
37
+ const legacyPath = path.join(projectRoot, 'donation-logs.sqlite');
38
+ if (!fs.existsSync(legacyPath)) return;
39
+ try {
40
+ fs.renameSync(legacyPath, targetPath);
41
+ console.log(`[migrate] Moved donation-logs.sqlite → ${targetPath}`);
42
+ } catch (e) {
43
+ try {
44
+ fs.copyFileSync(legacyPath, targetPath);
45
+ console.log(`[migrate] Copied donation-logs.sqlite → ${targetPath}`);
46
+ } catch (e2) {}
47
+ }
48
+ }
49
+
50
+ module.exports = {
51
+ APP_DIR,
52
+ LOGS_DIR,
53
+ BIN_DIR,
54
+ LOG_DB_PATH: path.join(LOGS_DIR, 'donation-logs.sqlite'),
55
+ DEFAULT_9ROUTER_DB,
56
+ CLOUDFLARED_BIN: path.join(BIN_DIR, process.platform === 'win32' ? 'cloudflared.exe' : 'cloudflared'),
57
+ ensureAppDirs,
58
+ migrateLegacyLogDb,
59
+ };
package/src/tray.js ADDED
@@ -0,0 +1,210 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const EventEmitter = require('events');
4
+ const { exec } = require('child_process');
5
+ const SysTrayImport = require('systray2');
6
+ const {
7
+ enableAutoStart,
8
+ disableAutoStart,
9
+ isAutoStartEnabled,
10
+ } = require('./autostart');
11
+
12
+ const SysTray = SysTrayImport.default || SysTrayImport;
13
+
14
+ function openBrowser(url) {
15
+ const platform = process.platform;
16
+ let cmd;
17
+
18
+ if (platform === 'darwin') {
19
+ cmd = `open "${url}"`;
20
+ } else if (platform === 'win32') {
21
+ cmd = `start "" "${url}"`;
22
+ } else {
23
+ cmd = `xdg-open "${url}"`;
24
+ }
25
+
26
+ return new Promise((resolve) => {
27
+ exec(cmd, { windowsHide: true }, (err) => {
28
+ if (err) {
29
+ console.log(`Open browser manually: ${url}`);
30
+ }
31
+ resolve();
32
+ });
33
+ });
34
+ }
35
+
36
+ class TrayManager extends EventEmitter {
37
+ constructor({ config, port }) {
38
+ super();
39
+ this.config = config;
40
+ this.port = Number(port) || 3456;
41
+ this.autoStartEnabled = Boolean(this.config.get('autoStart'));
42
+ this.tray = null;
43
+ this.menu = null;
44
+ this.autoStartItem = null;
45
+ this.autoStartPollTimer = null;
46
+ }
47
+
48
+ get appUrl() {
49
+ return `http://localhost:${this.port}/dashboard`;
50
+ }
51
+
52
+ get tooltip() {
53
+ return `Antigravity Token Collector - running on port ${this.port}`;
54
+ }
55
+
56
+ // Menu dibangun SEKALI di start(). Objek item (termasuk autoStartItem)
57
+ // dipertahankan agar __id internal systray2 stabil — klik selalu ter-resolve.
58
+ buildMenu() {
59
+ this.autoStartItem = {
60
+ title: this.autoStartItemTitle(),
61
+ tooltip: 'Run On OS startup',
62
+ checked: this.autoStartEnabled,
63
+ enabled: true,
64
+ };
65
+ return {
66
+ icon: this.getTrayIconBase64(),
67
+ isTemplateIcon: process.platform === 'darwin',
68
+ tooltip: this.tooltip,
69
+ items: [
70
+ {
71
+ title: `Running on :${this.port}`,
72
+ tooltip: this.tooltip,
73
+ enabled: false,
74
+ },
75
+ SysTray.separator,
76
+ {
77
+ title: 'Open Dashboard',
78
+ tooltip: 'Open dashboard in browser',
79
+ enabled: true,
80
+ },
81
+ SysTray.separator,
82
+ this.autoStartItem,
83
+ SysTray.separator,
84
+ {
85
+ title: 'Exit',
86
+ tooltip: 'Exit application',
87
+ enabled: true,
88
+ },
89
+ ],
90
+ };
91
+ }
92
+
93
+ autoStartItemTitle() {
94
+ return `Auto Start: ${this.autoStartEnabled ? 'ON' : 'OFF'}`;
95
+ }
96
+
97
+ getTrayIconBase64() {
98
+ const iconPath = path.join(__dirname, '..', 'web', 'public', 'favicon.ico');
99
+ return fs.readFileSync(iconPath).toString('base64');
100
+ }
101
+
102
+ async start() {
103
+ this.autoStartEnabled = await isAutoStartEnabled().catch(() => Boolean(this.config.get('autoStart')));
104
+ this.config.set('autoStart', this.autoStartEnabled);
105
+ this.ensureTrayBinaryExecutable();
106
+
107
+ this.menu = this.buildMenu();
108
+ this.tray = new SysTray({
109
+ menu: this.menu,
110
+ debug: false,
111
+ copyDir: false,
112
+ });
113
+
114
+ await this.tray.ready();
115
+ await this.tray.onClick(async (action) => {
116
+ await this.handleClick(action && action.item ? action.item : null);
117
+ });
118
+
119
+ // Auto Start bisa berubah dari dashboard; sinkronkan tiap 5 detik
120
+ // supaya status di tray selalu realtime.
121
+ this.autoStartPollTimer = setInterval(() => {
122
+ this.syncAutoStartState().catch(() => {});
123
+ }, 5000);
124
+ }
125
+
126
+ async syncAutoStartState(forceUpdate = false) {
127
+ const enabled = await isAutoStartEnabled().catch(() => null);
128
+ if (enabled === null) return;
129
+ if (!forceUpdate && enabled === this.autoStartEnabled) return;
130
+ this.autoStartEnabled = enabled;
131
+ this.config.set('autoStart', enabled);
132
+ await this.refreshAutoStartMenuItem();
133
+ }
134
+
135
+ // Update item Auto Start TANPA membangun ulang menu. PENTING: jangan pernah
136
+ // memakai action 'update-menu' — systray2 tidak memperbarui internalIdMap
137
+ // untuk menu baru, sehingga __id melenceng dan klik berikutnya gagal
138
+ // resolve (toggle terlihat "mati"). Dengan 'update-item' pada objek yang
139
+ // sama, __id tetap stabil dan checked/title ter-update di menubar.
140
+ async refreshAutoStartMenuItem() {
141
+ if (!this.tray || !this.autoStartItem) return;
142
+ this.autoStartItem.title = this.autoStartItemTitle();
143
+ this.autoStartItem.checked = this.autoStartEnabled;
144
+ try {
145
+ await this.tray.sendAction({
146
+ type: 'update-item',
147
+ item: this.autoStartItem,
148
+ });
149
+ } catch (error) {
150
+ console.error('[tray] update autostart item failed:', error.message);
151
+ }
152
+ }
153
+
154
+ ensureTrayBinaryExecutable() {
155
+ if (process.platform !== 'darwin') return;
156
+
157
+ const candidates = [
158
+ path.join(__dirname, '..', 'node_modules', 'systray2', 'traybin', 'tray_darwin_release'),
159
+ path.join(__dirname, '..', 'node_modules', 'systray2', 'traybin', 'tray_darwin'),
160
+ ];
161
+
162
+ for (const filePath of candidates) {
163
+ if (!fs.existsSync(filePath)) continue;
164
+ try {
165
+ fs.chmodSync(filePath, 0o755);
166
+ } catch (error) {}
167
+ }
168
+ }
169
+
170
+ async handleClick(item) {
171
+ if (!item || !item.title) return;
172
+
173
+ if (item.title === 'Open Dashboard') {
174
+ await openBrowser(this.appUrl || 'http://localhost:3456/dashboard');
175
+ return;
176
+ }
177
+
178
+ if (item.title.startsWith('Auto Start')) {
179
+ try {
180
+ const currentlyEnabled = await isAutoStartEnabled().catch(() => this.autoStartEnabled);
181
+ if (currentlyEnabled) await disableAutoStart();
182
+ else await enableAutoStart();
183
+ } catch (error) {
184
+ console.error('[tray] toggle autostart failed:', error.message);
185
+ }
186
+ // Baca ulang status sebenarnya lalu update item di menubar.
187
+ await this.syncAutoStartState(true);
188
+ return;
189
+ }
190
+
191
+ if (item.title === 'Exit') {
192
+ await this.stop();
193
+ // index.js mendengarkan event ini untuk cleanup (lock, tunnel) sebelum exit.
194
+ this.emit('exit');
195
+ if (this.listenerCount('exit') === 0) process.exit(0);
196
+ }
197
+ }
198
+
199
+ async stop() {
200
+ if (this.autoStartPollTimer) {
201
+ clearInterval(this.autoStartPollTimer);
202
+ this.autoStartPollTimer = null;
203
+ }
204
+ if (!this.tray) return;
205
+ await this.tray.kill(false);
206
+ this.tray = null;
207
+ }
208
+ }
209
+
210
+ module.exports = { TrayManager };