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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2026 decolua and contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,135 @@
1
+ <div align="center">
2
+
3
+ <img src="images/antigravity-tc.jpg" alt="Antigravity Token Collector" width="860" />
4
+
5
+ # Antigravity Token Collector
6
+
7
+ **Kumpulkan donasi token Antigravity dari siapa saja — langsung masuk ke database [9Router](https://github.com/decolua/9router) Anda.**
8
+
9
+ [![npm version](https://img.shields.io/npm/v/antigravity-tc.svg)](https://www.npmjs.com/package/antigravity-tc)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-22c55e.svg)](LICENSE)
11
+ [![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-14b8a6.svg)](#-dukungan-platform)
12
+ [![Node](https://img.shields.io/badge/node-%3E%3D20-fdba74.svg)]()
13
+
14
+ Tray app + dashboard untuk mengelola donasi token: halaman donasi publik, tunnel gratis tanpa expose IP, auto-start saat login, dan sync massal ke 9Router.
15
+
16
+ </div>
17
+
18
+ ---
19
+
20
+ ## ⚡ Quick Start
21
+
22
+ ```bash
23
+ npm install -g antigravity-tc
24
+ antigravity-tc
25
+ ```
26
+
27
+ 🎉 Dashboard terbuka di `http://localhost:3456/dashboard` — tray icon muncul di menubar.
28
+
29
+ __That's it!__ 🚀
30
+
31
+ > 💡 Password default: `1234567890` — hint tampil di halaman login selama belum diganti. Segera ganti di **Pengaturan → Keamanan**.
32
+ >
33
+ > 🔑 **Lupa password?** Jalankan `antigravity-tc-reset-password` di terminal untuk meresetnya kembali ke default.
34
+
35
+ ## 🔄 Cara Kerja
36
+
37
+ ```
38
+ ┌──────────────────┐ ┌─────────────────────┐ ┌──────────────────┐
39
+ │ Donatur │ │ ATC (Anda) │ │ 9Router │
40
+ │ │ │ │ │ │
41
+ │ Login Google ───┼──────▶ │ Cek eligibility ───┼──────▶ │ Token tersimpan │
42
+ │ lewat link yang │ │ Refresh otomatis │ │ siap dipakai │
43
+ │ Anda bagikan │ │ Anti-duplikat │ │ │
44
+ └──────────────────┘ └─────────────────────┘ └──────────────────┘
45
+ ```
46
+
47
+ 1. Jalankan `antigravity-tc` — dashboard & tray aktif.
48
+ 2. Buat **link donasi personal** dari dashboard (Generate Link), bagikan lewat chat mana pun — atau aktifkan **tunnel publik** gratis supaya bisa diakses dari luar tanpa expose IP.
49
+ 3. Donatur login Google + tempel kode — token otomatis dicek, di-refresh bila expired, lalu tersimpan ke 9Router.
50
+
51
+ ## 🖼️ Tampilan
52
+
53
+ | Dashboard | Sync Massal | Pengaturan |
54
+ | :---: | :---: | :---: |
55
+ | ![Dashboard](images/Dashboard.jpg) | ![Sync](images/Sync.jpg) | ![Pengaturan](images/Pengaturan.jpg) |
56
+
57
+ ## 📋 Fitur
58
+
59
+ | Fitur | Keterangan |
60
+ | --- | --- |
61
+ | 🌐 Halaman donasi publik | Login Google + tempel kode, token dicek eligibility lalu disimpan |
62
+ | 🔗 Generate link personal | `?for=Nama` — sapaan otomatis untuk tiap calon donatur |
63
+ | 📊 Dashboard admin | Statistik, tabel live, export & import CSV, generate link |
64
+ | 🔄 Sync massal ke 9Router | Upsert semua data sekaligus dengan log terminal realtime |
65
+ | ⚡ Auto refresh token | Token expired otomatis di-refresh Google saat check eligibility |
66
+ | 🌐 Tunnel publik gratis | Cloudflare Quick Tunnel — URL `*.trycloudflare.com` siap dibagikan |
67
+ | 🚀 Auto start | Berjalan otomatis (mode tray) saat OS login |
68
+ | 🔒 Keamanan | Login JWT (secret unik per instalasi), tunnel terkunci selama password default |
69
+
70
+ ## 📁 Data Location
71
+
72
+ Lokasi default **mengikuti OS tempat package di-install** — tanpa konfigurasi tambahan:
73
+
74
+ ### Database 9Router (target penyimpanan token)
75
+
76
+ | OS | Lokasi Default |
77
+ | --- | --- |
78
+ | macOS / Linux | `~/.9router/db/data.sqlite` |
79
+ | Windows | `%APPDATA%/9router/db/data.sqlite` |
80
+ | Docker | `/app/data/db/data.sqlite` (mount volume untuk persist) |
81
+
82
+ > Path ini bisa dioverride kapan saja via **Dashboard → Pengaturan → Database 9Router** (untuk instalasi non-standar) atau env `DB_PATH`.
83
+
84
+ ### Data ATC (log donasi, binary tunnel, config)
85
+
86
+ | OS | Lokasi |
87
+ | --- | --- |
88
+ | macOS / Linux | `~/.antigravity-tc/` |
89
+ | Windows | `%USERPROFILE%\.antigravity-tc\` |
90
+ | Docker | `/root/.antigravity-tc/` (mount `$HOME/.antigravity-tc` untuk persist) |
91
+
92
+ | Isi | Keterangan |
93
+ | --- | --- |
94
+ | `logs/donation-logs.sqlite` | Log donasi (auto-migrasi dari instalasi lama) |
95
+ | `bin/cloudflared` | Binary tunnel (auto-download per OS) |
96
+ | `logs/launcher.log` | Log saat auto-start |
97
+
98
+ ## 🖥️ Dukungan Platform
99
+
100
+ | Kemampuan | macOS | Linux | Windows | Docker |
101
+ | --- | :---: | :---: | :---: | :---: |
102
+ | Server + Dashboard + halaman donasi | ✅ | ✅ | ✅ | ✅ |
103
+ | Tray icon | ✅ | ✅ | ✅ | — |
104
+ | Auto-start saat login | ✅ | ✅ | ✅ | — (pakai restart policy) |
105
+ | Cloudflare Tunnel | ✅ | ✅ | ✅ | ✅ |
106
+ | Path DB 9Router otomatis per OS | ✅ | ✅ | ✅ | ✅ |
107
+
108
+ > Docker: jalankan dengan `antigravity-tc --hidden` sebagai entrypoint dan mount volume data (lihat tabel di atas). Deteksi Docker otomatis memakai path `/app/data/db/data.sqlite`.
109
+
110
+ ## 🔑 Password & `.env`
111
+
112
+ - Password: default `1234567890` → ganti via dashboard, atau set `APP_PASSWORD` di `.env`.
113
+ - Prioritas: password dari dashboard > `APP_PASSWORD` > default.
114
+ - Kredensial OAuth **sudah tertanam bawaan** (memakai kredensial publik Antigravity CLI) — `.env` hanya perlu diisi `PORT`/`APP_PASSWORD` bila mau. Override `OAUTH_CLIENT_ID`/`OAUTH_CLIENT_SECRET` tersedia untuk OAuth client sendiri.
115
+
116
+ ## ❓ Troubleshooting
117
+
118
+ | Masalah | Solusi |
119
+ | --- | --- |
120
+ | Lupa password admin | Jalankan `antigravity-tc-reset-password` (tambah `--yes` untuk tanpa konfirmasi) |
121
+ | Tunnel tidak bisa diaktifkan | Ganti password default dulu di Pengaturan → Keamanan |
122
+ | Tray icon tidak muncul | Pastikan binary `systray2/traybin/` executable |
123
+ | Sudah berjalan tapi tidak bisa dibuka lagi | App single-instance — cukup klik tray icon |
124
+ | Refresh token gagal | Cek koneksi; kredensial OAuth sudah bawaan — atau isi `OAUTH_CLIENT_ID` & `OAUTH_CLIENT_SECRET` di `.env` untuk client sendiri |
125
+ | DB 9Router tidak ketemu | Cek path di Pengaturan → Database 9Router |
126
+
127
+ ---
128
+
129
+ ## 🛠️ Development
130
+
131
+ Untuk menjalankan dari source, development, dan build/publish mandiri — lihat **[dev.md](dev.md)** dan **[build.md](build.md)**.
132
+
133
+ ## 📄 License
134
+
135
+ [MIT](LICENSE) © decolua and contributors
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ // Hard reset password admin ATC kembali ke default.
3
+ // Pakai: antigravity-tc-reset-password [--yes]
4
+
5
+ const path = require('path');
6
+ const http = require('http');
7
+ const readline = require('readline');
8
+
9
+ // .env selalu dibaca dari folder aplikasi (konsisten dengan index.js).
10
+ require('dotenv').config({ path: path.join(__dirname, '..', '.env'), quiet: true });
11
+
12
+ const { getConfig } = require('../src/config');
13
+
14
+ const DEFAULT_APP_PASSWORD = '1234567890';
15
+
16
+ function effectivePassword(config) {
17
+ return config.get('adminPassword') || process.env.APP_PASSWORD || DEFAULT_APP_PASSWORD;
18
+ }
19
+
20
+ function isAppRunning(port) {
21
+ return new Promise((resolve) => {
22
+ const request = http.get(`http://127.0.0.1:${port}/api/password-status`, (res) => {
23
+ res.resume();
24
+ resolve(res.statusCode === 200);
25
+ });
26
+ request.on('error', () => resolve(false));
27
+ request.setTimeout(1000, () => {
28
+ request.destroy();
29
+ resolve(false);
30
+ });
31
+ });
32
+ }
33
+
34
+ function ask(question) {
35
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
36
+ return new Promise((resolve) => {
37
+ rl.question(question, (answer) => {
38
+ rl.close();
39
+ resolve(answer.trim().toLowerCase());
40
+ });
41
+ });
42
+ }
43
+
44
+ async function main() {
45
+ const config = getConfig();
46
+ const configuredPort = Number(process.env.PORT || config.get('port') || 3456);
47
+ const customPassword = config.get('adminPassword');
48
+
49
+ console.log('========================================');
50
+ console.log(' Antigravity TC — Reset Password Admin');
51
+ console.log('========================================');
52
+ console.log('');
53
+
54
+ if (!customPassword && process.env.APP_PASSWORD) {
55
+ console.log('Password saat ini berasal dari APP_PASSWORD di file .env,');
56
+ console.log('bukan dari dashboard — reset di sini tidak akan mengubahnya.');
57
+ console.log('');
58
+ console.log('Solusi : hapus/ubah baris APP_PASSWORD di .env, lalu restart aplikasi.');
59
+ console.log(`Lokasi : ${path.join(__dirname, '..', '.env')}`);
60
+ process.exit(0);
61
+ }
62
+
63
+ if (!customPassword) {
64
+ console.log(`Password sudah default (${DEFAULT_APP_PASSWORD}). Tidak ada yang perlu direset.`);
65
+ process.exit(0);
66
+ }
67
+
68
+ const running = await isAppRunning(configuredPort);
69
+ if (running) {
70
+ console.log(`⚠️ Aplikasi sedang berjalan di port ${configuredPort}.`);
71
+ console.log(' Setelah reset, restart aplikasi (Exit dari tray lalu jalankan lagi)');
72
+ console.log(' supaya password baru terbaca.');
73
+ console.log('');
74
+ }
75
+
76
+ const skipConfirm = process.argv.includes('--yes') || process.argv.includes('-y');
77
+ if (!skipConfirm) {
78
+ const answer = await ask(`Reset password kembali ke default "${DEFAULT_APP_PASSWORD}"? (y/N): `);
79
+ if (answer !== 'y' && answer !== 'yes') {
80
+ console.log('Dibatalkan. Password tidak diubah.');
81
+ process.exit(0);
82
+ }
83
+ }
84
+
85
+ config.set('adminPassword', null);
86
+ console.log('');
87
+ console.log('✅ Password admin direset.');
88
+ console.log(` Password aktif sekarang: ${effectivePassword(config)}`);
89
+ if (running) console.log(' Jangan lupa restart aplikasinya.');
90
+ console.log('');
91
+ }
92
+
93
+ main().catch((error) => {
94
+ console.error('Gagal reset password:', error.message);
95
+ process.exit(1);
96
+ });
package/build.md ADDED
@@ -0,0 +1,82 @@
1
+ # 📦 Build & Publish Guide
2
+
3
+ Panduan build frontend, verifikasi paket, dan publish ke npmjs.com.
4
+
5
+ ## Build Frontend
6
+
7
+ Frontend (Vue 3 + Vite) harus di-build ke `web/dist/` — server Node melayaninya sebagai file statis.
8
+
9
+ ```bash
10
+ npm run build:web
11
+ ```
12
+
13
+ Hasil: `web/dist/` berisi `index.html` (halaman publik), `admin.html` (dashboard), `assets/*` (JS/CSS hashed), plus favicon/gambar dari `web/public/`.
14
+
15
+ > Tanpa `web/dist/`, halaman `/` dan `/dashboard` akan 404 dengan pesan "Jalankan npm run build:web".
16
+
17
+ ### Struktur Output
18
+
19
+ ```
20
+ web/dist/
21
+ ├── index.html ← halaman donasi publik (/)
22
+ ├── admin.html ← dashboard (/dashboard)
23
+ ├── favicon.ico, ex.jpeg, robots.txt
24
+ └── assets/ ← JS/CSS dengan hash (cache immutable)
25
+ ```
26
+
27
+ ## Sebelum Publish (Checklist)
28
+
29
+ 1. **Isi `author`** di `package.json` (nama/npm username Anda).
30
+ 2. **Naikkan versi**:
31
+
32
+ ```bash
33
+ npm version patch # 1.0.0 → 1.0.1 (bugfix)
34
+ npm version minor # 1.0.0 → 1.1.0 (fitur baru)
35
+ npm version major # 1.0.0 → 2.0.0 (breaking change)
36
+ ```
37
+
38
+ 3. **Lint bersih**: `npm run lint`
39
+ 4. **Verifikasi isi paket** (yang penting — pastikan secret tidak ikut):
40
+
41
+ ```bash
42
+ npm pack --dry-run
43
+ ```
44
+
45
+ Yang boleh ikut: `index.js`, `server.js`, `src/`, `web/dist/`, `web/public/`, `env.example`, `README.md`, `package.json`.
46
+ Yang TIDAK boleh ikut: `.env`, `*.sqlite`, `web/src/` (source frontend — pengguna cukup pakai dist).
47
+
48
+ Whitelist dikontrol field `files` di `package.json`.
49
+
50
+ ## Publish
51
+
52
+ ```bash
53
+ npm login # sekali saja
54
+ npm publish
55
+ ```
56
+
57
+ Script `prepublishOnly` otomatis menjalankan **lint + build:web** sebelum paket dibuat — build tidak akan basi.
58
+
59
+ Setelah publish, pengguna cukup:
60
+
61
+ ```bash
62
+ npm install -g antigravity-tc
63
+ antigravity-tc
64
+ ```
65
+
66
+ ## Setelah Publish — Verifikasi Cepat
67
+
68
+ ```bash
69
+ # di folder lain / mesin bersih
70
+ npm install -g antigravity-tc
71
+ antigravity-tc # dashboard + tray jalan
72
+ antigravity-tc --hidden # mode tray langsung
73
+ ```
74
+
75
+ Cek juga `~/.antigravity-tc/` terbuat dan halaman `/dashboard` terbuka.
76
+
77
+ ## Catatan Build
78
+
79
+ - **Vite multi-page**: konfigurasi `web/vite.config.mjs` (entry `index` + `admin`).
80
+ - **Tailwind via PostCSS**: `web/postcss.config.js` mengarah ke `web/tailwind.config.js` secara eksplisit; content glob memakai path absolut.
81
+ - **`vue` & `vite` adalah devDependencies** — pengguna paket tidak mendownload dependency frontend sama sekali; mereka hanya menerima `web/dist/`.
82
+ - **Icon tray** dibaca dari `web/public/favicon.ico` — folder ini ikut di-publish demi itu.
package/dev.md ADDED
@@ -0,0 +1,102 @@
1
+ # 🛠️ Development Guide
2
+
3
+ Panduan untuk menjalankan dari source, memodifikasi backend/frontend, dan konvensi proyek.
4
+
5
+ ## Prasyarat
6
+
7
+ - **Node.js ≥ 20** (diuji pada v24)
8
+ - **macOS** (tray icon, Keychain, dan LaunchAgent spesifik macOS)
9
+ - npm
10
+
11
+ ## Setup
12
+
13
+ ```bash
14
+ git clone <repo-anda> antigravity-tc
15
+ cd antigravity-tc
16
+ npm install
17
+ cp env.example .env # lalu isi nilai
18
+ npm run build:web # build frontend sekali sebelum jalan dari source
19
+ ```
20
+
21
+ File `.env` **selalu dibaca dari folder aplikasi** (bukan cwd) — jadi kredensial tetap terbaca walau dijalankan dari direktori lain (npm link / LaunchAgent).
22
+
23
+ ### Variabel `.env`
24
+
25
+ | Variabel | Keterangan |
26
+ | --- | --- |
27
+ | `PORT` | Port server (default `3456`) |
28
+ | `APP_PASSWORD` | Password admin (jika kosong: `1234567890`; password yang diganti via dashboard selalu menang) |
29
+ | `DB_PATH` | Path `data.sqlite` 9Router (opsional; bisa juga dari dashboard) |
30
+ | `OAUTH_CLIENT_ID` / `OAUTH_CLIENT_SECRET` | Opsional — kredensial OAuth publik Antigravity CLI sudah tertanam bawaan di `server.js` (base64); isi hanya untuk memakai client sendiri |
31
+ | `OAUTH_METHOD` | `tab` (default) atau `popup` |
32
+
33
+ ## Menjalankan
34
+
35
+ ```bash
36
+ npm start # TUI interaktif + tray
37
+ node index.js --hidden # langsung mode tray
38
+ npm run dev:web # frontend: Vite dev server di :5173 (proxy API ke :3456)
39
+ ```
40
+
41
+ Alur development frontend: jalankan `npm start` (backend) + `npm run dev:web` (frontend) di terminal berbeda, lalu buka `http://localhost:5173` — hot reload setiap edit `web/src/**`.
42
+
43
+ ## Struktur Proyek
44
+
45
+ ```
46
+ index.js Entry point: single-instance lock, tray, TUI, lifecycle
47
+ server.js HTTP server + semua API route
48
+ src/
49
+ paths.js Path ~/.antigravity-tc/ + migrasi database
50
+ config.js Config (library `conf`)
51
+ jwt.js Auth JWT — secret random per instalasi
52
+ tunnel.js Cloudflare Quick Tunnel (download binary, spawn, parse URL)
53
+ autostart.js LaunchAgent macOS (di tulis native, bukan via auto-launch)
54
+ tray.js Tray icon macOS (systray2)
55
+ cli.js TUI menu (@inquirer/prompts)
56
+ web/ Frontend Vue 3 + Vite + Tailwind (lihat build.md)
57
+ src/admin/ Dashboard admin (App, LoginCard, DashboardView, SettingsView, store.js)
58
+ src/public/ Halaman donasi publik (PKCE, modal eligibility)
59
+ src/shared/ Komponen & helper bersama (Mascot, api, toast)
60
+ ```
61
+
62
+ ## API Endpoints
63
+
64
+ Semua endpoint `/api/*` selain `verify-password`, `session`, `logout`, `password-status`, dan `oauth-config` membutuhkan **JWT admin** (cookie `admin_session` httpOnly).
65
+
66
+ | Endpoint | Method | Fungsi |
67
+ | --- | --- | --- |
68
+ | `/api/verify-password` | POST | Login → set cookie JWT |
69
+ | `/api/password` | POST | Ganti password (admin) |
70
+ | `/api/settings` | GET/POST | Baca/ubah pengaturan (path DB 9Router) |
71
+ | `/api/autostart` | POST | Toggle auto-start |
72
+ | `/api/tunnel` | GET/POST | Status / toggle tunnel publik |
73
+ | `/api/donations` | GET | List donasi + statistik (paginasi) |
74
+ | `/api/donations/import` | POST | Import CSV (dedupe by email) |
75
+ | `/api/donations/:id/check-eligibility` | POST | CE + auto refresh token |
76
+ | `/api/donations/:id/sync` | POST | Upsert ke 9Router by email |
77
+ | `/api/generate`, `/api/insert` | POST | Query SQL / insert manual (admin) |
78
+ | `/api/keychain`, `/api/decode` | POST | Ambil token dari Keychain (admin) |
79
+
80
+ ## Konvensi & Catatan Teknis
81
+
82
+ - **CommonJS** di backend (`require`/`module.exports`), ESLint flat config di `eslint.config.js` — `npm run lint` harus bersih sebelum commit/publish.
83
+ - **Priority insert 9Router**: selalu `COUNT(provider antigravity + authType oauth) + 1` (urutan terakhir), `isActive = 0`.
84
+ - **Import CSV**: email sama → update hanya jika data baru lebih baru (banding Expiry, lalu CreatedAt).
85
+ - **Tailwind**: content glob di `web/tailwind.config.js` memakai path absolut (`__dirname`) — jangan ubah ke relatif, karena resolve-nya bergantung cwd proses build.
86
+ - **JWT secret**: dibuat random sekali per instalasi, disimpan di config — tidak pernah ikut ter-publish.
87
+ - **Single instance**: lock di `$TMPDIR/antigravity-token-collector.lock`; instance kedua keluar dengan pesan "Sudah berjalan di menubar."
88
+ - Desain frontend: neobrutalist (hard shadow, border hitam tebal, warna flat) — design system ada di `web/src/styles.css`, maskot Astro-G di `web/src/shared/Mascot.vue`.
89
+
90
+ ## Testing Manual Cepat
91
+
92
+ ```bash
93
+ # login (password default)
94
+ curl -c ck.txt -X POST localhost:3456/api/verify-password \
95
+ -H 'Content-Type: application/json' -d '{"password":"1234567890"}'
96
+
97
+ # settings (harus 200)
98
+ curl -b ck.txt localhost:3456/api/settings
99
+
100
+ # tanpa cookie (harus 401)
101
+ curl localhost:3456/api/donations
102
+ ```
package/env.example ADDED
@@ -0,0 +1,12 @@
1
+ PORT=3456
2
+ APP_PASSWORD=
3
+ DB_PATH=
4
+
5
+ # Opsional — kredensial OAuth sudah tertanam bawaan (memakai kredensial publik
6
+ # Antigravity CLI), jadi biasanya TIDAK perlu diisi. Isi hanya jika ingin
7
+ # memakai OAuth client Anda sendiri:
8
+ # OAUTH_CLIENT_ID=
9
+ # OAUTH_CLIENT_SECRET=
10
+
11
+ # Options: tab (default), new_tab, or popup
12
+ OAUTH_METHOD=popup
Binary file
Binary file
Binary file
Binary file
package/index.js ADDED
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env node
2
+ const path = require('path');
3
+ // .env selalu dibaca dari folder aplikasi (bukan cwd) supaya kredensial OAuth
4
+ // tetap tersedia saat dijalankan via npm link / LaunchAgent dari folder mana pun.
5
+ require('dotenv').config({ path: path.join(__dirname, '.env'), quiet: true });
6
+
7
+ const fs = require('fs');
8
+ const os = require('os');
9
+ const { spawn } = require('child_process');
10
+ const lockfile = require('proper-lockfile');
11
+
12
+ const { getConfig } = require('./src/config');
13
+ const { isAutoStartEnabled } = require('./src/autostart');
14
+ const { TrayManager } = require('./src/tray');
15
+ const { showMainMenu } = require('./src/cli');
16
+
17
+ const config = getConfig();
18
+ const configuredPort = Number(process.env.PORT || config.get('port') || 3456);
19
+ process.env.PORT = String(configuredPort);
20
+
21
+ const {
22
+ startServer,
23
+ server,
24
+ getTunnelManager,
25
+ } = require('./server');
26
+
27
+ const LOCK_FILE_PATH = path.join(os.tmpdir(), 'antigravity-token-collector.lock');
28
+
29
+ let releaseLock = null;
30
+ let trayManager = null;
31
+ let menuOpen = false;
32
+ let isHandoffInProgress = false;
33
+
34
+ async function acquireSingleInstanceLock() {
35
+ if (!fs.existsSync(LOCK_FILE_PATH)) {
36
+ fs.writeFileSync(LOCK_FILE_PATH, 'lock\n', 'utf8');
37
+ }
38
+
39
+ try {
40
+ releaseLock = await lockfile.lock(LOCK_FILE_PATH, {
41
+ realpath: false,
42
+ retries: 0,
43
+ });
44
+ } catch (error) {
45
+ console.log('Sudah berjalan di menubar.');
46
+ process.exit(0);
47
+ }
48
+ }
49
+
50
+ async function cleanupAndExit(exitCode = 0) {
51
+ try {
52
+ const tunnelManager = getTunnelManager();
53
+ if (tunnelManager) {
54
+ // Matikan cloudflared tanpa mereset tunnelEnabled agar auto-start
55
+ // tunnel tetap bekerja pada launch berikutnya.
56
+ await tunnelManager.dispose();
57
+ }
58
+ } catch (error) {}
59
+
60
+ try {
61
+ if (trayManager) {
62
+ await trayManager.stop();
63
+ }
64
+ } catch (error) {}
65
+
66
+ try {
67
+ if (server.listening) {
68
+ await new Promise((resolve) => server.close(() => resolve()));
69
+ }
70
+ } catch (error) {}
71
+
72
+ try {
73
+ if (releaseLock) {
74
+ await releaseLock();
75
+ releaseLock = null;
76
+ }
77
+ } catch (error) {}
78
+
79
+ process.exit(exitCode);
80
+ }
81
+
82
+ async function releaseInstanceLock() {
83
+ if (!releaseLock) return;
84
+ try {
85
+ await releaseLock();
86
+ } catch (error) {}
87
+ releaseLock = null;
88
+ }
89
+
90
+ async function closeServer() {
91
+ if (!server.listening) return;
92
+ await new Promise((resolve) => server.close(() => resolve()));
93
+ }
94
+
95
+ async function handoffToBackground(port) {
96
+ if (isHandoffInProgress) return;
97
+ isHandoffInProgress = true;
98
+
99
+ const appUrl = `http://localhost:${port}`;
100
+
101
+ await closeServer();
102
+ await releaseInstanceLock();
103
+
104
+ const child = spawn(process.execPath, [path.join(__dirname, 'index.js'), '--hidden'], {
105
+ cwd: process.cwd(),
106
+ detached: true,
107
+ stdio: 'ignore',
108
+ env: {
109
+ ...process.env,
110
+ PORT: String(port),
111
+ },
112
+ });
113
+ child.unref();
114
+
115
+ console.log('⏳ Switching to tray mode... (icon already visible in menu bar)');
116
+ console.log(`🔔 Antigravity-tc (PID: ${child.pid})`);
117
+ console.log(` Dashboard: ${appUrl}/dashboard`);
118
+ console.log('');
119
+ console.log('💡 You can close this terminal. Klik icon tray untuk quit.');
120
+
121
+ process.exit(0);
122
+ }
123
+
124
+ async function openTerminalMenu(menuContext) {
125
+ if (menuOpen) return;
126
+ menuOpen = true;
127
+ try {
128
+ await showMainMenu(menuContext);
129
+ } finally {
130
+ menuOpen = false;
131
+ }
132
+ }
133
+
134
+ async function main() {
135
+ await acquireSingleInstanceLock();
136
+
137
+ const autoStart = await isAutoStartEnabled().catch(() => false);
138
+ config.set('autoStart', autoStart);
139
+ config.set('port', configuredPort);
140
+
141
+ const port = await startServer(configuredPort);
142
+ const appUrl = `http://localhost:${port}/dashboard`;
143
+
144
+ trayManager = new TrayManager({ config, port });
145
+ await trayManager.start();
146
+ trayManager.on('exit', () => cleanupAndExit(0));
147
+
148
+ const menuContext = {
149
+ trayManager,
150
+ appUrl,
151
+ onHideToTray: () => handoffToBackground(port),
152
+ onExit: () => cleanupAndExit(0),
153
+ };
154
+
155
+ const hidden = process.argv.includes('--hidden');
156
+ if (!hidden) {
157
+ await openTerminalMenu(menuContext);
158
+ }
159
+ }
160
+
161
+ process.on('SIGINT', () => {
162
+ cleanupAndExit(0);
163
+ });
164
+
165
+ process.on('SIGTERM', () => {
166
+ cleanupAndExit(0);
167
+ });
168
+
169
+ process.on('uncaughtException', (error) => {
170
+ console.error('Uncaught exception:', error);
171
+ cleanupAndExit(1);
172
+ });
173
+
174
+ process.on('unhandledRejection', (error) => {
175
+ console.error('Unhandled rejection:', error);
176
+ cleanupAndExit(1);
177
+ });
178
+
179
+ main().catch((error) => {
180
+ console.error('Failed to start:', error);
181
+ cleanupAndExit(1);
182
+ });