Compare commits

..
92 Commits
Author SHA1 Message Date
semantic-release-bot 51a93c5c88 chore(release): 0.8.0 [skip ci]
## [0.8.0](https://github.com/Draculabo/AntigravityManager/compare/v0.7.0...v0.8.0) (2026-02-07)

### ✨ Features

* complete account-bound profile switching and hardening ([#78](https://github.com/Draculabo/AntigravityManager/issues/78)) ([a93c6d0](https://github.com/Draculabo/AntigravityManager/commit/a93c6d0cea5b9904a30234faffe767505f753373))

### 🐛 Bug Fixes

* **ci:** increase Node heap for publish step to prevent macOS OOM ([#76](https://github.com/Draculabo/AntigravityManager/issues/76)) ([ee64179](https://github.com/Draculabo/AntigravityManager/commit/ee641799c27425ceb6d2d6d00a132881bedb1f04))
* **ci:** make WiX Toolset setup resilient on Windows runners ([#73](https://github.com/Draculabo/AntigravityManager/issues/73)) ([5fe434f](https://github.com/Draculabo/AntigravityManager/commit/5fe434f7050cb8975d65502baeb03b1747e00611))

### 📝 Documentation

* **openspec:** backfill missing proposals ([287e848](https://github.com/Draculabo/AntigravityManager/commit/287e848b3291d2ee43b173802b87491c2a90930d))
2026-02-07 17:13:38 +00:00
Draculabo 287e848b32 docs(openspec): backfill missing proposals 2026-02-08 01:12:17 +08:00
vin a93c6d0cea feat: complete account-bound profile switching and hardening (#78) 2026-02-08 01:10:07 +08:00
vin ee641799c2 fix(ci): increase Node heap for publish step to prevent macOS OOM (#76) 2026-02-06 23:57:16 +08:00
vin 5fe434f705 fix(ci): make WiX Toolset setup resilient on Windows runners (#73) 2026-02-06 23:13:48 +08:00
semantic-release-bot cda59ed3a2 chore(release): 0.7.0 [skip ci]
## [0.7.0](https://github.com/Draculabo/AntigravityManager/compare/v0.6.0...v0.7.0) (2026-02-06)

### ✨ Features

* add multi-arch release artifacts and MSI packaging ([#65](https://github.com/Draculabo/AntigravityManager/issues/65)) ([f572ae4](https://github.com/Draculabo/AntigravityManager/commit/f572ae4652937efb25ab66defcfa55ddf65ac484))

### 🐛 Bug Fixes

* restore account switching on Antigravity 1.16.5 and migrate db sync to drizzle ([#69](https://github.com/Draculabo/AntigravityManager/issues/69)) ([ed94abf](https://github.com/Draculabo/AntigravityManager/commit/ed94abf0d6e90c0a1bf9c76f1ec3fdea091f6c4b))

### 📝 Documentation

* translate repository documentation to English ([#72](https://github.com/Draculabo/AntigravityManager/issues/72)) ([18389b9](https://github.com/Draculabo/AntigravityManager/commit/18389b975392f63441ede8369f13f6ef068214ab))

### ♻️ Code Refactoring

* migrate to winston and enable daily rotated app logs ([#71](https://github.com/Draculabo/AntigravityManager/issues/71)) ([2ae2216](https://github.com/Draculabo/AntigravityManager/commit/2ae2216f1ed3fa503ffd87a14cafd3921077a5c4))
2026-02-06 14:15:26 +00:00
vin 18389b9753 docs: translate repository documentation to English (#72) 2026-02-06 22:14:06 +08:00
vin 2ae2216f1e refactor: migrate to winston and enable daily rotated app logs (#71) 2026-02-06 21:10:54 +08:00
vin ed94abf0d6 fix: restore account switching on Antigravity 1.16.5 and migrate db sync to drizzle (#69) 2026-02-06 20:16:02 +08:00
vin f572ae4652 feat: add multi-arch release artifacts and MSI packaging (#65) 2026-02-05 22:25:00 +08:00
semantic-release-bot 36848b46dc chore(release): 0.6.0 [skip ci]
## [0.6.0](https://github.com/Draculabo/AntigravityManager/compare/v0.5.0...v0.6.0) (2026-02-04)

### ✨ Features

*  add cloud reset time UI ([#56](https://github.com/Draculabo/AntigravityManager/issues/56)) ([f6f8069](https://github.com/Draculabo/AntigravityManager/commit/f6f8069ce4673ae7027c6ba248daa18c8b602218))
* windows install guidance ([#63](https://github.com/Draculabo/AntigravityManager/issues/63)) ([ce71470](https://github.com/Draculabo/AntigravityManager/commit/ce7147025d37648a82f6f6b300502ef4462bfde6))

### 🐛 Bug Fixes

* correct Windows install notice path ([5bda4b1](https://github.com/Draculabo/AntigravityManager/commit/5bda4b19dfc9ce64735cd39600d41a5deefc26ee))
* **proxy:** route Claude Code CLI requests on /v1/chat/completions to Anthropic handler ([#61](https://github.com/Draculabo/AntigravityManager/issues/61)) ([476d297](https://github.com/Draculabo/AntigravityManager/commit/476d297e4b2dd3015f7ebcbb915b643730816dc6))
2026-02-04 12:35:15 +00:00
Draculabo 5bda4b19df fix: correct Windows install notice path 2026-02-04 20:33:26 +08:00
saianilsingi 476d297e4b fix(proxy): route Claude Code CLI requests on /v1/chat/completions to Anthropic handler (#61)
* fix(proxy): route Claude models from chat completions to anthropic handler

* style(proxy): minor formatting cleanup
2026-02-04 18:22:53 +08:00
vin ce7147025d feat: windows install guidance (#63)
* build: add dmg/appimage and artifact metadata

* feat: enhance packaging outputs and add Windows install-path notice
2026-02-04 18:10:26 +08:00
vin 080fb67974 chore: add Windows ARM64 publish runner and update readme (#57) 2026-02-01 18:08:13 +08:00
vin f6f8069ce4 feat: add cloud reset time UI (#56)
* feat: show quota reset time on cloud account card

* chore: ignore changelog file  in prettier

* style: format account card
2026-02-01 18:03:27 +08:00
Draculabo 6328378da5 chore: make release manual and publish on release 2026-02-01 17:55:06 +08:00
semantic-release-bot d25f2948c3 chore(release): 0.5.0 [skip ci]
## [0.5.0](https://github.com/Draculabo/AntigravityManager/compare/v0.4.0...v0.5.0) (2026-01-30)

### ✨ Features

* **i18n:** add Russian localization ([#48](https://github.com/Draculabo/AntigravityManager/issues/48)) ([63956c9](https://github.com/Draculabo/AntigravityManager/commit/63956c9c2d60f829a998237abe6ade675fdb01ed))
* Implement collapsible sidebar and refined status bar UI ([#45](https://github.com/Draculabo/AntigravityManager/issues/45)) ([1265d04](https://github.com/Draculabo/AntigravityManager/commit/1265d044f69e52fba7da72fde6c47a0b85c58232))
* sentry integration ([#51](https://github.com/Draculabo/AntigravityManager/issues/51)) ([a785640](https://github.com/Draculabo/AntigravityManager/commit/a785640d7a51b65a6852383401bd7c284b716975))
2026-01-30 05:01:40 +00:00
63956c9c2d feat(i18n): add Russian localization (#48)
* feat(i18n): add Russian localization

* fix: missing i18n keys

---------

Co-authored-by: Violetta <[email protected]>
Co-authored-by: Draculabo <[email protected]>
2026-01-30 12:50:37 +08:00
vin a785640d7a feat: sentry integration (#51)
* feat: integrate sentry error reporting

* feat: report logger errors to sentry with 30s log context
2026-01-30 11:48:49 +08:00
Sujit BiswasandDraculabo 1265d044f6 feat: Implement collapsible sidebar and refined status bar UI (#45)
* chore: mark peer dependencies in package-lock.json

* docs: add comprehensive developer guide for antigravity-manager

* build: add @radix-ui/react-tooltip dependency

The tooltip component is needed for future UI enhancements to provide better user guidance and information display.

* feat(StatusBar): add collapsed view with toggle button

- Introduce `isCollapsed` prop to support both expanded and compact layouts
- Collapsed view shows a single icon button with status tooltip
- Enhanced visual styling with shadows, borders, and improved spacing
- Use Tooltip component to display status in collapsed mode

* feat(ui): add Radix UI tooltip component wrapper

Create a reusable tooltip component using Radix UI primitives.
The wrapper provides styled content with animations and integrates
with the project's class name utility for consistent theming.

* feat(layout): add collapsible sidebar with persistent state

- Add toggle button to collapse/expand sidebar with smooth animations
- Persist sidebar state in localStorage to maintain user preference
- Show tooltips for navigation items when sidebar is collapsed
- Adjust StatusBar component to support collapsed state

* chore: remove "peer": true flags from package-lock.json

* build: mark peer dependencies in package-lock.json

* build: add yaml v2.8.2 dependency and update package-lock

* docs(project_overview): update skill documentation formatting

* docs: remove extra blank line in AGENTS.md

* docs: update changelog formatting to use hyphens

* docs: improve readability of OpenSpec workflow files

* chore: standardize YAML string quotes in GitHub configs

* style(StatusBar): reformat JSX for consistency and readability

- Simplify className expres

* style(ui): standardize quotes and class order in tooltip component

- Use single quotes consistently across imports and strings
- Reorder Tailwind CSS classes to follow recommended conventions

* style(ipc): remove unnecessary parentheses in debug log statement

* style(MainLayout): improve code formatting and consistency

- Reformat multi-line import for better readability
- Normalize Tailwind CSS class ordering
- Remove unnecessary whitespace and ensure consistent quotes
- Maintain all functionality while improving code style

* style(i18n): format long translation strings for better readability

* style: remove unnecessary line break in logger.info call

The logger.info call had an unnecessary line break that made the code less readable. The line has been consolidated to improve code style consistency.

* chore: disable several ESLint rules for development flexibility

Disable TypeScript ESLint rules and other linting rules that are too restrictive during active development. This allows faster iteration without constant linting errors for common patterns like `any` types, unused variables, and inline comments.

* refactor: use const for immutable variables in request mapper

- Change `let` to `const` for variables that are not reassigned
- Improves code clarity and prevents accidental reassignment

* refactor: replace direct hasOwnProperty calls with safe variant

Use Object.prototype.hasOwnProperty.call to avoid issues when objects have overridden hasOwnProperty method. This ensures consistent behavior across all object types.

* fix: disable eslint for intentional assignment in useEffect

The eslint warning was triggered by directly using setProxyConfig with config.proxy inside useEffect without including it in the dependency array. This assignment is intentional and only needs to run when config changes, not when setProxyConfig changes. Adding the eslint-disable comment prevents the warning while maintaining correct behavior.

* chore: disable react-hooks/exhaustive-deps rule

This rule was already disabled; the change only adds a trailing comma to the ESLint configuration for consistency.

* fix(eslint): re-enable lint rules for hooks and case-declarations

---------

Co-authored-by: Draculabo <[email protected]>
2026-01-28 18:55:40 +08:00
semantic-release-bot 9ca0ad0396 chore(release): 0.4.0 [skip ci]
## [0.4.0](https://github.com/Draculabo/AntigravityManager/compare/v0.3.5...v0.4.0) (2026-01-28)

### ✨ Features

* add system autostart and single-instance support ([ea51253](https://github.com/Draculabo/AntigravityManager/commit/ea51253d589abd537682344d3bdb684b8fc9a511))
* implement smart foreground quota refresh with debounce ([dd9e84a](https://github.com/Draculabo/AntigravityManager/commit/dd9e84a0dbefad6066193b6bd468689a755a02e3))

### 🐛 Bug Fixes

* stub nestjs optional modules for packaging ([f0eb7c6](https://github.com/Draculabo/AntigravityManager/commit/f0eb7c6b619a3ea9ea203d66f5dbce731d731e3c))
2026-01-28 08:17:50 +00:00
Draculabo f0eb7c6b61 fix: stub nestjs optional modules for packaging 2026-01-28 16:16:27 +08:00
Draculabo ea51253d58 feat: add system autostart and single-instance support 2026-01-28 13:57:18 +08:00
Draculabo dd9e84a0db feat: implement smart foreground quota refresh with debounce
- Trigger immediate quota poll when main window gains focus.
- Add 10s debounce to prevent API spam on frequent window switching.
- Add concurrency guard to prevent overlapping poll requests.
- Reset background polling interval after manual refresh.
- Add unit tests for polling, debounce, and concurrency logic.
2026-01-28 12:58:35 +08:00
semantic-release-bot a988a33d21 chore(release): 0.3.5 [skip ci]
## [0.3.5](https://github.com/Draculabo/AntigravityManager/compare/v0.3.4...v0.3.5) (2026-01-26)

### 🐛 Bug Fixes

* "Check Quota Now" button not refreshing UI after polling ([#42](https://github.com/Draculabo/AntigravityManager/issues/42)) ([e959ee3](https://github.com/Draculabo/AntigravityManager/commit/e959ee346e7c26a8a4c5b7deefa5bd2452153f9d))

### 📝 Documentation

* remove beta download links from README ([5a21680](https://github.com/Draculabo/AntigravityManager/commit/5a2168030eac4ddeffa1c3b002b2de48b6a11a8f))
2026-01-26 20:39:04 +00:00
Sujit BiswasandDraculabo e959ee346e fix: "Check Quota Now" button not refreshing UI after polling (#42)
* feat: add React Query hooks for managing cloud accounts and update the generated route tree.

* feat: implement React Query hooks for comprehensive cloud account management.

* fix(cloud-accounts): add error handling to force poll mutation

Add an onError handler to log failures when forcing a cloud monitor poll, improving debuggability.

* fix: add error handling for cloud account polling failure

Add error toast notification when force polling fails, providing localized error message to improve user feedback.

* refactor: remove error logging from force poll cloud monitor hook

The onError handler was removed as error handling is now managed at a higher level in the application. This simplifies the hook and prevents duplicate error logging.

* fix(localization): add missing pollFailed translation keys

Add missing English and Chinese translations for the 'pollFailed' error message used when quota polling fails for all accounts.

* fix(CloudAccountList): prevent duplicate force poll requests

Add guard clause to disable button while mutation is pending to avoid duplicate API calls.

* fix: ensure UI updates after cloud quota poll and refresh

* chore: remove "peer": true flags from package-lock.json

---------

Co-authored-by: Draculabo <[email protected]>
2026-01-27 04:38:24 +08:00
Draculabo 5a2168030e docs: remove beta download links from README 2026-01-27 03:22:52 +08:00
semantic-release-bot 00bf4dea0b chore(release): 0.3.4 [skip ci]
## [0.3.4](https://github.com/Draculabo/AntigravityManager/compare/v0.3.3...v0.3.4) (2026-01-26)

### 🐛 Bug Fixes

* **security:** add safeStorage fallback for production builds ([#38](https://github.com/Draculabo/AntigravityManager/issues/38)) ([#43](https://github.com/Draculabo/AntigravityManager/issues/43)) ([0208058](https://github.com/Draculabo/AntigravityManager/commit/02080588b764ed88a5831152a3a1249f1d077d29))

### 📝 Documentation

* update beta release link ([d5ee08d](https://github.com/Draculabo/AntigravityManager/commit/d5ee08d5a06a915a8b82f680b38e2f532105498c))
2026-01-26 19:06:28 +00:00
vin 02080588b7 fix(security): add safeStorage fallback for production builds (#38) (#43)
Summary
  - Replace hard keytar dependency with a multi-strategy key storage:
    1) safeStorage (Electron built-in) — preferred
    2) keytar (system keychain) — fallback
    3) file-based encryption — last resort
  - Dynamically import keytar to avoid asar load failures
  - Refactor cloudHandler to use const (clean code)
  - Fix “Failed to load cloud accounts” in packaged app

  Rationale
  The keytar native module can fail to load from asar archives in production builds. This ensures the app works even when system keychain access is unavailable.

  Additional changes
  - Replace sync file ops with async (fs/promises)
  - Add atomic file writes to prevent data races
  - Implement concurrency lock for key generation
  - Add security error codes for better diagnostics
  - Add warnings for file-based fallback mode
  - Align error handling after merge
  - Migrate legacy encrypted account data

  References
  - Issue: #38
  - Commit: 92dc2f6
2026-01-27 03:05:41 +08:00
Draculabo d5ee08d5a0 docs: update beta release link 2026-01-26 02:10:10 +08:00
semantic-release-bot e844aa9545 chore(release): 0.3.3 [skip ci]
## [0.3.3](https://github.com/Draculabo/AntigravityManager/compare/v0.3.2...v0.3.3) (2026-01-25)

### 🐛 Bug Fixes

* accept lowercase antigravity in process detection ([0d4e2ab](https://github.com/Draculabo/AntigravityManager/commit/0d4e2ab21f37704e09ef1a67c181c48b42df1180))

### 📝 Documentation

* add beta download link to readme ([f15bb48](https://github.com/Draculabo/AntigravityManager/commit/f15bb48fdda10fda3c2382941ee0ce51204f750a))
* clean up changelog duplicate ([22265e1](https://github.com/Draculabo/AntigravityManager/commit/22265e153c9d394229aa48afdc5948044b74e842))
2026-01-25 17:39:31 +00:00
Draculabo 0d4e2ab21f fix: accept lowercase antigravity in process detection 2026-01-26 01:37:58 +08:00
Draculabo f15bb48fdd docs: add beta download link to readme 2026-01-26 01:17:59 +08:00
Draculabo 515648d64d chore(release): add changelog link, PR list, and contributor links to release notes 2026-01-25 22:10:14 +08:00
Draculabo 22265e153c docs: clean up changelog duplicate 2026-01-25 19:08:53 +08:00
semantic-release-bot ad39a735b5 chore(release): 0.3.2 [skip ci]
## [0.3.2](https://github.com/Draculabo/AntigravityManager/compare/v0.3.1...v0.3.2) (2026-01-25)

### 🐛 Bug Fixes

* handle keychain hint and suppress pgrep spam ([bd3d41a](https://github.com/Draculabo/AntigravityManager/commit/bd3d41aed17bafe9d684c5c421bad8b90afa19a8))

### 📝 Documentation

* add macOS self-signing workaround for Keychain issues ([01e3f8f](https://github.com/Draculabo/AntigravityManager/commit/01e3f8f8fd6dacc5eed214ed4b505d6d85f4bcff))

### 🔧 Continuous Integration

* setup semantic release configuration and github actions workflow ([d2945a6](https://github.com/Draculabo/AntigravityManager/commit/d2945a6e8a14d75f577716183cdff093443d9636))
* trigger publish on release published event ([6a07bc0](https://github.com/Draculabo/AntigravityManager/commit/6a07bc0a10a5ad802777e007cfd7390852119b15))
2026-01-25 10:36:15 +00:00
Draculabo 6a07bc0a10 ci: trigger publish on release published event 2026-01-25 18:35:20 +08:00
semantic-release-bot e91457eceb chore(release): 0.3.2 [skip ci]
## [0.3.2](https://github.com/Draculabo/AntigravityManager/compare/v0.3.1...v0.3.2) (2026-01-25)

### 🐛 Bug Fixes

* handle keychain hint and suppress pgrep spam ([bd3d41a](https://github.com/Draculabo/AntigravityManager/commit/bd3d41aed17bafe9d684c5c421bad8b90afa19a8))

### 📝 Documentation

* add macOS self-signing workaround for Keychain issues ([01e3f8f](https://github.com/Draculabo/AntigravityManager/commit/01e3f8f8fd6dacc5eed214ed4b505d6d85f4bcff))

### 🔧 Continuous Integration

* setup semantic release configuration and github actions workflow ([d2945a6](https://github.com/Draculabo/AntigravityManager/commit/d2945a6e8a14d75f577716183cdff093443d9636))
2026-01-25 09:48:18 +00:00
Draculabo 01e3f8f8fd docs: add macOS self-signing workaround for Keychain issues 2026-01-25 17:47:13 +08:00
Draculabo bd3d41aed1 fix: handle keychain hint and suppress pgrep spam 2026-01-25 17:46:32 +08:00
Draculabo d2945a6e8a ci: setup semantic release configuration and github actions workflow 2026-01-25 05:07:39 +08:00
Draculabo 89f39710c7 chore(release): bump version to 0.3.1
Publish Release / publish (ubuntu-latest) (push) Failing after 5m31s
Publish Release / publish (macos-14) (push) Canceled after 0s
Publish Release / publish (macos-latest) (push) Canceled after 0s
Publish Release / publish (windows-latest) (push) Canceled after 0s
2026-01-25 04:00:32 +08:00
Draculabo 1bce5d331d fix: enable WAL mode and force initialization on startup 2026-01-25 03:53:13 +08:00
Draculabo 47be175e27 fix: enable WAL mode to resolve process resource contention 2026-01-25 03:31:59 +08:00
Draculabo f0718db374 fix: resolve startup race condition causing cloud accounts verify failure 2026-01-25 02:45:05 +08:00
Draculabo 4b12f1220d chore(release): bump version to 0.3.0
Publish Release / publish (ubuntu-latest) (push) Failing after 6m48s
Publish Release / publish (macos-14) (push) Canceled after 0s
Publish Release / publish (macos-latest) (push) Canceled after 0s
Publish Release / publish (windows-latest) (push) Canceled after 0s
2026-01-23 10:58:18 +08:00
Draculabo 47c14763de chore(release): bump version to 0.3.0-rc.3
Publish Release / publish (ubuntu-latest) (push) Failing after 5m51s
Publish Release / publish (macos-14) (push) Canceled after 0s
Publish Release / publish (macos-latest) (push) Canceled after 0s
Publish Release / publish (windows-latest) (push) Canceled after 0s
2026-01-21 20:48:27 +08:00
Draculabo 37a9750df2 feat: Improved process detection on macOS/Linux using find-process to reliably identify the main application 2026-01-21 20:48:03 +08:00
Draculabo f708de24c0 fix: correct Linux state.vscdb path to include User/globalStorage 2026-01-21 18:21:06 +08:00
Draculabo 6e7f2252ad chore(release): bump version to 0.3.0-rc.2
Publish Release / publish (ubuntu-latest) (push) Failing after 6m1s
Publish Release / publish (macos-14) (push) Canceled after 0s
Publish Release / publish (macos-latest) (push) Canceled after 0s
Publish Release / publish (windows-latest) (push) Canceled after 0s
2026-01-21 17:23:08 +08:00
Draculabo e96d3e0e8e fix: add macOS arm64 build for Apple Silicon support 2026-01-21 17:15:17 +08:00
Draculabo 12ab4e0ce7 chore(release): bump version to 0.3.0-rc.1
Publish Release / publish (ubuntu-latest) (push) Failing after 5m46s
Publish Release / publish (macos-latest) (push) Canceled after 0s
Publish Release / publish (windows-latest) (push) Canceled after 0s
2026-01-21 01:05:56 +08:00
Draculabo 013f3290f3 chore: add VS Code settings for auto-formatting and ESLint 2026-01-21 00:43:54 +08:00
Draculabo f224fea081 feat: add expiration warning for Google OAuth authentication 2026-01-21 00:43:00 +08:00
Draculabo a438070268 feat: add button to open logs folder 2026-01-21 00:42:49 +08:00
Draculabo e927ac9506 feat: verify Google OAuth code automatically after receipt 2026-01-21 00:37:42 +08:00
Draculabo 465eb5d0a4 fix: tray icon not appearing in production builds
Publish Release / publish (ubuntu-latest) (push) Failing after 7m3s
Publish Release / publish (macos-latest) (push) Canceled after 0s
Publish Release / publish (windows-latest) (push) Canceled after 0s
- Use extraResource config to copy assets outside of ASAR package
- Add debug logging for tray icon path resolution
- Bump version to 0.2.2
2026-01-19 21:08:31 +08:00
Draculabo 37b0fdeeb0 chore(release): bump version to 0.2.1 2026-01-19 20:20:08 +08:00
Draculabo a343e0c1ae chore:: apply prettier formatting to entire codebase 2026-01-19 19:40:11 +08:00
Draculabo 695d787b01 chore: exclude .agents and openspec from formatting 2026-01-19 19:34:43 +08:00
Draculabo b4ca037518 fix: resolve CI failures (lint, format, tests) 2026-01-19 19:31:46 +08:00
c43507c716 Fix: Linux tray quit and zombie icons (#24)
* Fix: Linux tray quit and zombie icons (#23)

- Add before-quit handler to set isQuitting flag
- Change pgrep -x to pgrep -xi for case-insensitive matching on Linux
- Add antigravity-manager to self-kill exclusion filter
- Destroy existing tray before creating new one in initTray()
- Destroy tray in will-quit handler
- Export destroyTray function for cleanup

Fixes #23

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* Fix: Address code review feedback

- Add icon validation before creating tray (isEmpty check)
- Add try/catch around tray.destroy() calls for error handling
- Add try/catch around destroyTray() in will-quit handler
- Add explicit !manager check in Linux process filter for defensive coding

Co-Authored-By: Claude Opus 4.5 <[email protected]>

---------

Co-authored-by: Jirka <[email protected]>
Co-authored-by: Claude Opus 4.5 <[email protected]>
2026-01-19 16:11:48 +08:00
Draculabo f5c44c549f chore: release v0.2.0
Publish Release / publish (ubuntu-latest) (push) Failing after 6m12s
Publish Release / publish (macos-latest) (push) Canceled after 0s
Publish Release / publish (windows-latest) (push) Canceled after 0s
2026-01-16 22:26:55 +08:00
Draculabo f37127d385 build: conditionally include plugins based on start command in forge.config.ts 2026-01-16 22:00:33 +08:00
Draculabo f636dd61a5 feat: enhance cloudHandler to inject minimal auth state when database entry is missing 2026-01-16 20:30:49 +08:00
Draculabo 42973561fb chore: upgrade electron from 32.3.3 to 37.3.1 2026-01-16 02:58:26 +08:00
Draculabo 5772e2e90e feat: implement stability fixes and enhance error handling 2026-01-16 02:51:29 +08:00
Draculabo b0ec96d4a6 fix: Converting circular structure to JSON 2026-01-15 22:58:05 +08:00
Draculabo 5a004d9d16 docs: add curly brace constraints for conditional statements 2026-01-15 22:54:23 +08:00
Draculabo ccc5c91969 docs: fix incorrect reference documentation name 2026-01-15 22:37:35 +08:00
Draculabo dcfed908a5 chore: reelease 0.1.1
Publish Release / publish (ubuntu-latest) (push) Failing after 5m50s
Publish Release / publish (macos-latest) (push) Canceled after 0s
Publish Release / publish (windows-latest) (push) Canceled after 0s
2026-01-11 20:48:27 +08:00
Draculabo 1336de679f fix: resolve Antigravity visibility issue on account switch 2026-01-11 20:39:29 +08:00
Draculabo bc43cc101c docs: add AGENTS.md for AI assistant guidance 2026-01-10 04:45:59 +08:00
Draculabo 01f6a5aaee docs: update v0.1.0 changelog 2026-01-10 04:41:39 +08:00
Draculabo f8e06efcca docs: update readme 2026-01-10 04:35:37 +08:00
Draculabo 13f02c7ac2 feat: update logo image 2026-01-10 04:26:07 +08:00
Draculabo b425dc3758 feat: update license 2026-01-10 04:24:03 +08:00
Draculabo fb13826afd feat: identity injection 2026-01-10 04:08:55 +08:00
Draculabo 166a92ab94 chore: release 0.1.0
Publish Release / publish (ubuntu-latest) (push) Failing after 5m52s
Publish Release / publish (macos-latest) (push) Canceled after 0s
Publish Release / publish (windows-latest) (push) Canceled after 0s
2026-01-10 02:36:47 +08:00
Draculabo 2a961f8520 fix: nestjs build error 2026-01-10 02:36:23 +08:00
Draculabo db675821b8 chore: add electron images 2026-01-10 02:35:56 +08:00
Draculabo c182a514bf chore: add generate-assets script 2026-01-10 02:35:38 +08:00
Draculabo a079cf5ccc fix: ci build error 2026-01-10 01:17:48 +08:00
Draculabo 2b443251bb chore: add grpc dependencies 2026-01-10 01:07:09 +08:00
Draculabo 7768cf5309 fix: button size 2026-01-10 01:07:09 +08:00
Draculabo 64e27ab821 feat: integrate antigravity adapter 2026-01-10 01:07:09 +08:00
Draculabo f5a85e3436 feat: support LAN network 2026-01-10 01:07:09 +08:00
Draculabo 098cbe3eb9 fix: server di error 2026-01-10 01:07:09 +08:00
Draculabo bc775d4fd6 feat: add boss key 2026-01-10 01:07:09 +08:00
Draculabo e7087bc4b1 feat: migrate gateway to nestjs 2025-12-24 23:26:55 +08:00
Draculabo c4a0fa773f docs: update project document 2025-12-24 23:11:54 +08:00
232 changed files with 31157 additions and 3072 deletions

No files matched your search

+88
View File
@@ -0,0 +1,88 @@
---
name: antigravity-manager
description: Comprehensive guide to Antigravity Manager architecture, workflows, and development. Use this to understand how to work on the project.
---
# Antigravity Manager Developer Guide
## 🏗️ Architecture Overview
Antigravity Manager is a hybrid Desktop Application built with Electron, React, and NestJS. It follows a modular architecture where the frontend (Renderer) communicates with the backend (Main) via type-safe IPC (ORPC).
```mermaid
graph TD
User[User Interface] -->|React/Vite| Renderer[Renderer Process]
Renderer -->|ORPC Client| IPC[IPC Layer]
IPC -->|ORPC Router| Main[Main Process]
Main -->|Bootstraps| Server[NestJS Server]
Main -->|Calls| Services[Service Layer]
Services -->|Read/Write| DB[(SQLite Database)]
Services -->|HTTP| Cloud[Cloud APIs (Google/Anthropic)]
```
### Key Technologies
- **Frontend**: React 19, TailwindCSS v4, TanStack Router, TanStack Query.
- **Backend**: Electron (Main), NestJS (Core Logic), Better-SQLite3 (Data).
- **Communication**: ORPC (Type-safe IPC wrapper around Electron IPC).
- **Build**: Electron Forge + Vite.
## 📂 Directory Structure
- `src/main.ts`: Electron Main Process entry point.
- `src/preload.ts`: Bridge between Main and Renderer.
- `src/renderer.tsx`: React App entry point.
- `src/components/`: Reusable React UI components (Radix UI based).
- `src/ipc/`: IPC Routers and Handlers (Domain logic).
- `router.ts`: Main ORPC router definition.
- `account/`, `cloud/`, `database/`: Domain-specific handlers.
- `src/server/`: NestJS application modules (proxies/gateways).
- `src/services/`: Core business logic (framework agnostic).
- `GoogleAPIService.ts`: Gemini/Cloud interactions.
- `AutoSwitchService.ts`: Account rotation logic.
- `src/routes/`: Frontend routing definitions (File-based).
## 🚀 Development Workflow
### Prerequisites
- Node.js 18+
- npm (Project uses `package-lock.json`)
### Common Commands
- **Start Dev Server**: `npm start`
- **Lint Code**: `npm run lint`
- **Unit Test**: `npm run test:unit`
- **E2E Test**: `npm run test:e2e`
- **Build Production**: `npm run make`
## 🧠 Core Concepts
### IPC Communication (ORPC)
The project uses `orpc` for type-safe communication.
- **Define**: Create a router in `src/ipc/router.ts` with Zod schemas.
- **Implement**: Add logic in handlers (e.g., `src/ipc/account/handler.ts`).
- **Call**: Use the generated client in React components.
### Database Access
Data is stored in a local SQLite file (`test.db` in dev, user data in prod).
- Use `Better-SQLite3` for direct access.
- Logic should be encapsulated in `src/services` or `src/ipc`.
### Account Management
- Accounts are added via OAuth (Google/Claude).
- `GoogleAPIService` handles token exchange and refreshing.
- `AutoSwitchService` monitors usage and switches active accounts automatically.
## ⚠️ Critical Rules
1. **Type Safety**: strict TypeScript usage; Zod for runtime validation.
2. **Components**: Use `src/components/ui` (Radix primitives) for consistency.
3. **Async**: Handle all IPC/DB calls asynchronously with try/catch.
4. **Security**: Never commit secrets. API keys are user-provided or encrypted locally.
+5
View File
@@ -1,14 +1,18 @@
---
description: Implement an approved OpenSpec change and keep tasks in sync.
---
<!-- OPENSPEC:START -->
**Guardrails**
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
- Keep changes tightly scoped to the requested outcome.
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
**Steps**
Track these steps as TODOs and complete them one by one.
1. Read `changes/<id>/proposal.md`, `design.md` (if present), and `tasks.md` to confirm scope and acceptance criteria.
2. Work through tasks sequentially, keeping edits minimal and focused on the requested change.
3. Confirm completion before updating statuses—make sure every item in `tasks.md` is finished.
@@ -16,5 +20,6 @@ Track these steps as TODOs and complete them one by one.
5. Reference `openspec list` or `openspec show <item>` when additional context is required.
**Reference**
- Use `openspec show <id> --json --deltas-only` if you need additional context from the proposal while implementing.
<!-- OPENSPEC:END -->
+5
View File
@@ -1,13 +1,17 @@
---
description: Archive a deployed OpenSpec change and update specs.
---
<!-- OPENSPEC:START -->
**Guardrails**
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
- Keep changes tightly scoped to the requested outcome.
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
**Steps**
1. Determine the change ID to archive:
- If this prompt already includes a specific change ID (for example inside a `<ChangeId>` block populated by slash-command arguments), use that value after trimming whitespace.
- If the conversation references a change loosely (for example by title or summary), run `openspec list` to surface likely IDs, share the relevant candidates, and confirm which one the user intends.
@@ -19,6 +23,7 @@ description: Archive a deployed OpenSpec change and update specs.
5. Validate with `openspec validate --strict` and inspect with `openspec show <id>` if anything looks off.
**Reference**
- Use `openspec list` to confirm change IDs before archiving.
- Inspect refreshed specs with `openspec list --specs` and address any validation issues before handing off.
<!-- OPENSPEC:END -->
+5
View File
@@ -1,8 +1,11 @@
---
description: Scaffold a new OpenSpec change and validate strictly.
---
<!-- OPENSPEC:START -->
**Guardrails**
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
- Keep changes tightly scoped to the requested outcome.
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
@@ -10,6 +13,7 @@ description: Scaffold a new OpenSpec change and validate strictly.
- Do not write any code during the proposal stage. Only create design documents (proposal.md, tasks.md, design.md, and spec deltas). Implementation happens in the apply stage after approval.
**Steps**
1. Review `openspec/project.md`, run `openspec list` and `openspec list --specs`, and inspect related code or docs (e.g., via `rg`/`ls`) to ground the proposal in current behaviour; note any gaps that require clarification.
2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, and `design.md` (when needed) under `openspec/changes/<id>/`.
3. Map the change into concrete capabilities or requirements, breaking multi-scope efforts into distinct spec deltas with clear relationships and sequencing.
@@ -19,6 +23,7 @@ description: Scaffold a new OpenSpec change and validate strictly.
7. Validate with `openspec validate <id> --strict` and resolve every issue before sharing the proposal.
**Reference**
- Use `openspec show <id> --json --deltas-only` or `openspec show <spec> --type spec` to inspect details when validation fails.
- Search existing requirements with `rg -n "Requirement:|Scenario:" openspec/specs` before writing new ones.
- Explore the codebase with `rg <keyword>`, `ls`, or direct file reads so proposals align with current implementation realities.
+40
View File
@@ -0,0 +1,40 @@
---
name: 🐛 Bug Report
about: Report a bug to help us improve
title: '[Bug] '
labels: bug
assignees: ''
---
## Describe the Bug
A clear and concise description of what the bug is.
## Steps To Reproduce
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See error
## Expected Behavior
A clear and concise description of what you expected to happen.
## Actual Behavior
What actually happened.
## Screenshots
If applicable, add screenshots to help explain your problem.
## Environment
- **OS**: [e.g., Windows 11, macOS Sonoma, Ubuntu 22.04]
- **App Version**: [e.g., 0.0.1]
- **Node.js Version**: [e.g., 18.17.0]
## Additional Context
Add any other context about the problem here.
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: 📖 Documentation
url: https://github.com/Draculabo/AntigravityManager#readme
about: Check our README for usage information
- name: ❓ Questions
url: https://github.com/Draculabo/AntigravityManager/discussions
about: Ask questions and discuss with the community
+29
View File
@@ -0,0 +1,29 @@
---
name: 💡 Feature Request
about: Suggest an idea for this project
title: '[Feature] '
labels: enhancement
assignees: ''
---
## Is your feature request related to a problem?
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
## Describe the solution you'd like
A clear and concise description of what you want to happen.
## Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.
## Additional context
Add any other context or screenshots about the feature request here.
## Are you willing to contribute?
- [ ] Yes, I'd like to implement this feature
- [ ] Yes, I can help with testing
- [ ] No, I'm just suggesting
+3 -3
View File
@@ -1,6 +1,6 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: "weekly"
interval: 'weekly'
+1 -1
View File
@@ -1,7 +1,7 @@
name-template: 'v$RESOLVED_VERSION'
tag-template: 'v$RESOLVED_VERSION'
categories:
- title: '🚀 What''s new'
- title: "🚀 What's new"
labels:
- 'feature'
- 'feat'
+46 -7
View File
@@ -1,9 +1,8 @@
name: Publish Release
on:
push:
tags:
- 'v*'
release:
types: [published]
workflow_dispatch:
jobs:
@@ -11,7 +10,28 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]
include:
- os: windows-latest
platform: win32
arch: x64
- os: windows-11-arm
platform: win32
arch: arm64
- os: macos-15-intel
platform: darwin
arch: x64
- os: macos-14
platform: darwin
arch: arm64
- os: macos-14
platform: darwin
arch: universal
- os: ubuntu-latest
platform: linux
arch: x64
- os: ubuntu-24.04-arm
platform: linux
arch: arm64
runs-on: ${{ matrix.os }}
@@ -29,15 +49,34 @@ jobs:
cache: 'npm'
- name: Install Linux dependencies
if: matrix.os == 'ubuntu-latest'
if: matrix.platform == 'linux'
run: |
sudo apt-get update
sudo apt-get install -y rpm
sudo apt-get install -y fakeroot rpm libsecret-1-dev pkg-config
- name: Ensure WiX Toolset
if: matrix.platform == 'win32' && matrix.arch == 'x64'
shell: powershell
run: |
$installed = choco list --local-only wixtoolset --exact --limit-output
if (-not $installed) {
choco install wixtoolset --version=3.14.0 -y
} elseif ($installed -match '^wixtoolset\|3\.14(\.|$)') {
Write-Host "WiX Toolset already compatible: $installed"
} else {
Write-Host "Incompatible WiX Toolset detected: $installed"
choco upgrade wixtoolset --version=3.14.0 --allow-downgrade -y
}
- name: Install dependencies
run: npm ci
- name: Publish Release
env:
NODE_OPTIONS: --max-old-space-size=8192
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm run publish
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
run: npm run publish -- --platform=${{ matrix.platform }} --arch=${{ matrix.arch }}
+34
View File
@@ -0,0 +1,34 @@
name: Release
on:
workflow_dispatch:
permissions:
contents: write
issues: write
pull-requests: write
jobs:
release:
name: Semantic Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for semantic-release to analyze commits
persist-credentials: false # We use a token in the next steps
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} # Use GH_TOKEN to trigger other workflows
run: npx semantic-release
+3
View File
@@ -102,3 +102,6 @@ out/
# Tanstack
.tanstack/
# Sentry Config File
.env.sentry-build-plugin
+4 -1
View File
@@ -3,4 +3,7 @@
/coverage
/.vite
README.md
/src/routeTree.gen.ts
CHANGELOG.md
/src/routeTree.gen.ts
.agents/
openspec/
-1
View File
@@ -8,5 +8,4 @@
"tabWidth": 2,
"proseWrap": "preserve",
"tailwindFunctions": ["clsx", "tw"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"]
}
+501 -1
View File
@@ -1,18 +1,518 @@
<!-- OPENSPEC:START -->
# OpenSpec Instructions
These instructions are for AI assistants working in this project.
Always open `@/openspec/AGENTS.md` when the request:
- Mentions planning or proposals (words like proposal, spec, change, plan)
- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
- Sounds ambiguous and you need the authoritative spec before coding
Use `@/openspec/AGENTS.md` to learn:
- How to create and apply change proposals
- Spec format and conventions
- Project structure and guidelines
Keep this managed block so 'openspec update' can refresh the instructions.
<!-- OPENSPEC:END -->
<!-- OPENSPEC:END -->
# User Guide
This document is a user-level guide for AI assistants working in this repository. It standardizes language, tool/script preferences, and development notes for the current tech stack.
## 💬 Communication Conventions
- **Language**: Use English consistently for conversation, TODOs, and code-related content (comments, UI copy, commit messages, PR descriptions, and similar artifacts).
- **Conclusion first**: Start with the core conclusion/summary, then provide details.
- **References**: When citing code, always provide full file paths (for example, `src/main.ts:42`).
## 💻 Runtime and Tooling
- **Runtime**: Node.js (Electron environment)
- **Node**: Recommended Node.js 20+
- **Package manager**: `npm` (this project includes `package-lock.json`; use npm only)
- **Build tools**: Electron Forge + Vite
- **Terminal**: Windows (PowerShell) / VSCode MCP tools can be used safely
## 🧩 Tech Stack Overview
- **Frontend**:
- React 19, TypeScript
- Tailwind CSS v4, `clsx`, `tailwind-merge`, `tailwindcss-animate`
- Radix UI (Primitives), Lucide React (Icons), Simple Icons (`@icons-pack/react-simple-icons`)
- `class-variance-authority` (CVA), `react-i18next` + `i18next`
- TanStack Router (Routing), TanStack Query (State Management)
- Components: Modular design under `src/components`
- **Backend (Electron Main/Server)**:
- Electron (Main/Preload/Renderer architecture)
- NestJS (internal proxy/gateway service, started by main process)
- Better-SQLite3 (local database), Drizzle ORM / Raw SQL
- ORPC (type-safe RPC)
- gRPC (`@grpc/grpc-js`, `@grpc/proto-loader`)
- Logging: `winston` + `winston-daily-rotate-file`
- Zod (validation)
- **Testing**:
- Vitest (unit/integration), Testing Library
- Playwright (E2E)
## 📁 Directory Structure
```plaintext
.
├─ src/
│ ├─ actions/ # App actions and flow orchestration
│ ├─ assets/ # Static assets
│ ├─ components/ # React UI components (base components under ui/)
│ ├─ constants/ # Constants
│ ├─ hooks/ # Custom React hooks
│ ├─ ipc/ # Electron IPC logic (Database, Config, etc.)
│ ├─ layouts/ # Layout components
│ ├─ lib/ # Shared low-level utilities
│ ├─ localization/ # i18n translation resources
│ ├─ mocks/ # Mock data for tests and development
│ ├─ routes/ # TanStack Router route definitions
│ ├─ server/ # NestJS backend logic (Gateway/Proxy)
│ ├─ services/ # Service layer
│ ├─ styles/ # Global styles (Tailwind classes)
│ ├─ tests/ # Test code
│ ├─ types/ # TypeScript type definitions
│ ├─ utils/ # Utility functions
│ ├─ App.tsx # React app entry
│ ├─ main.ts # Electron main entry
│ ├─ preload.ts # Electron preload script
│ └─ renderer.ts # Electron renderer entry
├─ forge.config.ts # Electron Forge config
└─ package.json
```
## 🧱 Component Architecture
- **Modular components**: Each component should have its own directory, with at least a `.tsx` file and optional styles/subcomponents.
- **Shared capabilities**: General helpers in `src/utils/`; low-level shared wrappers in `src/lib/`.
- **Service layer**: Centralize data access in `src/services/` or `src/ipc/`; frontend should consume IPC or RPC only.
## 📦 Common Scripts
Use `npm` for all commands:
- **Development (Dev)**:
- `npm start` - Start Electron dev environment (Electron Forge)
- `npm run lint` - Run ESLint checks
- `npm run format` - Run Prettier check
- `npm run format:write` - Auto-format with Prettier
- `npm run type-check` - Run TypeScript type check
- **Build**:
- `npm run package` - Package app (application bundle only)
- `npm run make` - Build and generate distributable installers
- `npm run publish` - Publish app
- **Testing**:
- `npm test` - Run Vitest tests
- `npm run test:watch` - Run Vitest in watch mode
- `npm run test:unit` - Same as above for unit-focused runs
- `npm run test:e2e` - Run Playwright E2E tests
- `npm run test:all` - Run all tests
### Running a Single Test
- Unit test: `npm run test:unit path/to/test.test.ts`
- E2E test: `npm run test:e2e path/to/test.spec.ts`
- Type check: `npm run type-check`
## 🧪 Development Notes
- **Build**: Build stage may ignore TS/ESLint errors depending on project/CI configuration.
- **DevTools**: `code-inspector-plugin` is integrated; use `Shift + Click` on page elements to jump to source code.
- **React**: React Strict Mode is disabled.
- **NestJS**: Runs as an Electron child process; logs are visible in main-process console.
## Security and Data
- **Security**: Never commit secrets; use environment variables for sensitive config; validate all user input; encrypt sensitive data.
- **Database**: Use Better-SQLite3; encapsulate operations in services layer; always use prepared statements; test DB operations independently.
- **i18n**: Use `react-i18next`; keys should use kebab-case; translation files are stored in `src/localization/`.
## 📝 Conventions
- **File naming**:
- Components: PascalCase (for example, `Button.tsx`)
- Tools/config: camelCase or kebab-case
- **Import paths**: Use `@/` alias for `src/`.
- **Type safety**: Avoid `any`; enforce end-to-end type safety with Zod + TypeScript.
- **Component design**:
- Prefer Radix UI Primitives.
- Use Tailwind utility classes; avoid CSS Modules unless necessary.
- **API communication**: Frontend should prioritize ORPC client or IPC for strong type inference.
### Naming Specifics
- **Functions/Variables**: camelCase (for example, `handleClick`, `isCurrent`)
- **Constants**: UPPER_SNAKE_CASE (for example, `LOCAL_STORAGE_KEYS`)
- **Files**:
- Services: `ServiceName.service.ts`
- Types: `type-name.ts`
### Import Organization
```typescript
// 1. React and core libraries
import React, { useEffect } from 'react';
import { createRoot } from 'react-dom/client';
// 2. External dependencies (alphabetical order)
import { useTranslation } from 'react-i18next';
import { formatDistanceToNow } from 'date-fns';
// 3. Internal imports (using @ alias)
import { Account } from '@/types/account';
import { Card, CardContent } from '@/components/ui/card';
```
### Component Structure
```typescript
// 1. Imports
import React, { useState } from 'react';
// 2. Type definitions
interface ComponentProps { /* props */ }
// 3. Component implementation
export const Component: React.FC<ComponentProps> = ({ prop1 }) => {
// 4. Hooks
const { t } = useTranslation();
// 5. Render
return <div>{/* JSX */}</div>;
};
```
> Before commit, run `npm run lint` and `npm run format`.
## 📝 Terminal Output and References
- Prefer code blocks; avoid Markdown tables and Mermaid unless necessary.
- If tables are used, keep them left-aligned and check display consistency.
Example:
```plaintext
+------+---------+---------+
| ID | Name | Role |
+------+---------+---------+
| 1 | Alice | Admin |
| 2 | Bob | User |
+------+---------+---------+
```
### Reference Rules
- External resources: use full clickable links (issues, docs, API references).
- Source code location: use full file paths (optionally with line numbers).
Example:
```plaintext
- "resolveFilePath owns this logic"
- "VSCode has a known limitation in undo behavior"
References:
- resolveFilePath: src/utils/workspace.ts:40
- VSCode undo limitation: https://github.com/microsoft/vscode/issues/77190
```
## 🏷️ Markdown Writing
- Always specify a language for fenced code blocks; use `plaintext` if unsure.
- Keep one blank line after headings for readability.
## Line-Break Rule
`return` and similar statements should not share a line with other statements. Keep them on separate lines.
## 💭 Commenting Rules
- Required comment scenarios: complex business logic/algorithms, non-obvious behaviors, important design tradeoffs, and key reference links.
- Principles:
- Explain **why**, not **what**, and not changelog history.
- Update comments whenever related code changes.
- Prefer JSDoc; for complex functions, start with high-level overview, then annotate key steps (1, 2, 3...).
- Keep spacing between English and Chinese words if both appear for readability; do not comment deleted legacy code.
Quality self-check: six months later, what useful context does a new teammate gain from this comment? If the answer is "none", remove it.
Example:
```typescript
/**
* Handle payment request with multi-step validation.
*/
function processPayment(request: PaymentRequest) {
// 1. Input validation
// 2. Risk evaluation (low/medium/high paths)
// 3. Gateway call
// 4. User notification
}
export enum BudgetType {
Free = 'free',
/** ✅ Prefer JSDoc over end-of-line comments */
Package = 'package',
}
```
## 🛠️ Development Guide
### General Principles
- Prioritize stability and maintainability before optimization.
- For uncertainty, state assumptions/tradeoffs/validation approach clearly, then implement.
- Trust agreed preconditions; avoid excessive defensive coding against guaranteed invariants.
- Refactor legacy code conservatively; use modern approaches for new features where appropriate.
- Avoid premature optimization: implement simple and direct first; optimize only when justified.
- Always use braces for control flow (`if`, `while`, and similar statements).
### Error Handling
```typescript
// Use try-catch for async operations
try {
const result = await someOperation();
return result;
} catch (error) {
console.error('Operation failed:', error);
throw new Error('Failed to complete operation');
}
// Use proper error typing
if (error instanceof Error) {
/* handle Error instance */
}
```
### New Feature Implementation
- Code should be clear, readable, reusable, efficient, and testable.
- Prefer mature and reliable modern APIs.
### Refactoring and Bug Fixing
- Prefer incremental changes; align scope first before large refactors.
- Preserve existing structure and style; avoid over-abstraction risk.
### Development Lifecycle Checklist
Exploration / planning:
- \[ ] Fully understand requirements; break down into 3-6 steps
- \[ ] Review documentation and existing solutions first
- \[ ] Validate ideas by reading actual code
- \[ ] Build a TODO list
Implementation / refactor / fix:
- [ ] Review related templates and surrounding code; follow existing patterns
- [ ] Fail fast on invalid inputs/states
- [ ] Improve frontend interaction and UX within constraints
Acceptance / validation:
- \[ ] Validate implementation through tests or temporary scripts
- \[ ] After multiple incremental edits, evaluate whether changes should be consolidated
- \[ ] Run quality checks
- \[ ] Update related docs
Summary / output:
- \[ ] Verify output formatting requirements
- \[ ] List deviations from plan and key decisions for human review
- \[ ] Provide optimization suggestions
- \[ ] Include full references at the end
## 🔍 Code Quality and Lint
- Use descriptive variable names (`mutationObserver`, `button`, `element`) and avoid `mo`, `btn`, `el`.
- Check for missing critical comments and keep comment language consistent.
- Use VSCode MCP diagnostics for TS/ESLint issues and fix key findings.
- If tests are added/updated, run and fix them before submission.
## ⛔ Operations Requiring Explicit Confirmation
- Running destructive commands
- Executing `git commit` or `git push`
- Creating new test files (maintainer review required first)
## 🔧 Tool Preferences and Commands
Packages and scripts:
- `npm install` (or `npm i`)
Shell:
- Run commands in repository root.
- Quote file paths when appropriate.
Web search:
- Use `WebSearch` for latest information; use `mcp__SearXNG__search` when needed.
Documentation/usage lookup:
- Use `context7` for latest dependency usage.
VSCode MCP (if available):
- `mcp__vscode-mcp__get_references` for refactor impact analysis
- `mcp__vscode-mcp__rename_symbol` for safe renaming
- `mcp__vscode-mcp__get_symbol_lsp_info` for types/signatures/definitions
## 🚨 Local Quality Checks (Optional Flow)
After a set of changes, run these three checks in parallel instead of full lint immediately:
```plaintext
Task(subagent_type: "quick-code-review", description: "Code review", prompt: "[change description]")
Task(subagent_type: "diagnostics", description: "Diagnostics", prompt: "[same as above]")
Task(subagent_type: "run-related-tests", description: "Run tests", prompt: "[same as above]")
```
`change description` example:
```plaintext
- Modified files: list of relative paths
- Context: requirement/business background
```
Flow: initial check -> fix key issues -> re-check -> iterate until key issues are resolved.
Note: these tools are read-only analyzers; you still need to apply fixes manually. Pass precise file paths, not broad directories.
<skills_system priority="1">
## Available Skills
<!-- SKILLS_TABLE_START -->
<usage>
When users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.
How to use skills:
- Invoke: Bash("openskills read <skill-name>")
- The skill content will load with detailed instructions on how to complete the task
- Base directory provided in output for resolving bundled resources (references/, scripts/, assets/)
Usage notes:
- Only use skills listed in <available_skills> below
- Do not invoke a skill that is already loaded in your context
- Each skill invocation is stateless
</usage>
<available_skills>
<skill>
<name>algorithmic-art</name>
<description>Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.</description>
<location>project</location>
</skill>
<skill>
<name>brand-guidelines</name>
<description>Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.</description>
<location>project</location>
</skill>
<skill>
<name>canvas-design</name>
<description>Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.</description>
<location>project</location>
</skill>
<skill>
<name>doc-coauthoring</name>
<description>Guide users through a structured workflow for co-authoring documentation. Use when user wants to write documentation, proposals, technical specs, decision docs, or similar structured content. This workflow helps users efficiently transfer context, refine content through iteration, and verify the doc works for readers. Trigger when user mentions writing docs, creating proposals, drafting specs, or similar documentation tasks.</description>
<location>project</location>
</skill>
<skill>
<name>docx</name>
<description>"Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. When Claude needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks"</description>
<location>project</location>
</skill>
<skill>
<name>frontend-design</name>
<description>Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.</description>
<location>project</location>
</skill>
<skill>
<name>internal-comms</name>
<description>A set of resources to help me write all kinds of internal communications, using the formats that my company likes to use. Claude should use this skill whenever asked to write some sort of internal communications (status reports, leadership updates, 3P updates, company newsletters, FAQs, incident reports, project updates, etc.).</description>
<location>project</location>
</skill>
<skill>
<name>mcp-builder</name>
<description>Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).</description>
<location>project</location>
</skill>
<skill>
<name>pdf</name>
<description>Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms. When Claude needs to fill in a PDF form or programmatically process, generate, or analyze PDF documents at scale.</description>
<location>project</location>
</skill>
<skill>
<name>pptx</name>
<description>"Presentation creation, editing, and analysis. When Claude needs to work with presentations (.pptx files) for: (1) Creating new presentations, (2) Modifying or editing content, (3) Working with layouts, (4) Adding comments or speaker notes, or any other presentation tasks"</description>
<location>project</location>
</skill>
<skill>
<name>skill-creator</name>
<description>Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.</description>
<location>project</location>
</skill>
<skill>
<name>slack-gif-creator</name>
<description>Knowledge and utilities for creating animated GIFs optimized for Slack. Provides constraints, validation tools, and animation concepts. Use when users request animated GIFs for Slack like "make me a GIF of X doing Y for Slack."</description>
<location>project</location>
</skill>
<skill>
<name>theme-factory</name>
<description>Toolkit for styling artifacts with a theme. These artifacts can be slides, docs, reportings, HTML landing pages, etc. There are 10 pre-set themes with colors/fonts that you can apply to any artifact that has been creating, or can generate a new theme on-the-fly.</description>
<location>project</location>
</skill>
<skill>
<name>web-artifacts-builder</name>
<description>Suite of tools for creating elaborate, multi-component claude.ai HTML artifacts using modern frontend web technologies (React, Tailwind CSS, shadcn/ui). Use for complex artifacts requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX artifacts.</description>
<location>project</location>
</skill>
<skill>
<name>webapp-testing</name>
<description>Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.</description>
<location>project</location>
</skill>
<skill>
<name>xlsx</name>
<description>"Comprehensive spreadsheet creation, editing, and analysis with support for formulas, formatting, data analysis, and visualization. When Claude needs to work with spreadsheets (.xlsx, .xlsm, .csv, .tsv, etc) for: (1) Creating new spreadsheets with formulas and formatting, (2) Reading or analyzing data, (3) Modify existing spreadsheets while preserving formulas, (4) Data analysis and visualization in spreadsheets, or (5) Recalculating formulas"</description>
<location>project</location>
</skill>
</available_skills>
<!-- SKILLS_TABLE_END -->
</skills_system>
+217
View File
@@ -0,0 +1,217 @@
<a name="readme-top"></a>
# Changelog
## [0.8.0](https://github.com/Draculabo/AntigravityManager/compare/v0.7.0...v0.8.0) (2026-02-07)
### ✨ Features
* complete account-bound profile switching and hardening ([#78](https://github.com/Draculabo/AntigravityManager/issues/78)) ([a93c6d0](https://github.com/Draculabo/AntigravityManager/commit/a93c6d0cea5b9904a30234faffe767505f753373))
### 🐛 Bug Fixes
* **ci:** increase Node heap for publish step to prevent macOS OOM ([#76](https://github.com/Draculabo/AntigravityManager/issues/76)) ([ee64179](https://github.com/Draculabo/AntigravityManager/commit/ee641799c27425ceb6d2d6d00a132881bedb1f04))
* **ci:** make WiX Toolset setup resilient on Windows runners ([#73](https://github.com/Draculabo/AntigravityManager/issues/73)) ([5fe434f](https://github.com/Draculabo/AntigravityManager/commit/5fe434f7050cb8975d65502baeb03b1747e00611))
### 📝 Documentation
* **openspec:** backfill missing proposals ([287e848](https://github.com/Draculabo/AntigravityManager/commit/287e848b3291d2ee43b173802b87491c2a90930d))
## [0.7.0](https://github.com/Draculabo/AntigravityManager/compare/v0.6.0...v0.7.0) (2026-02-06)
### ✨ Features
* add multi-arch release artifacts and MSI packaging ([#65](https://github.com/Draculabo/AntigravityManager/issues/65)) ([f572ae4](https://github.com/Draculabo/AntigravityManager/commit/f572ae4652937efb25ab66defcfa55ddf65ac484))
### 🐛 Bug Fixes
* restore account switching on Antigravity 1.16.5 and migrate db sync to drizzle ([#69](https://github.com/Draculabo/AntigravityManager/issues/69)) ([ed94abf](https://github.com/Draculabo/AntigravityManager/commit/ed94abf0d6e90c0a1bf9c76f1ec3fdea091f6c4b))
### 📝 Documentation
* translate repository documentation to English ([#72](https://github.com/Draculabo/AntigravityManager/issues/72)) ([18389b9](https://github.com/Draculabo/AntigravityManager/commit/18389b975392f63441ede8369f13f6ef068214ab))
### ♻️ Code Refactoring
* migrate to winston and enable daily rotated app logs ([#71](https://github.com/Draculabo/AntigravityManager/issues/71)) ([2ae2216](https://github.com/Draculabo/AntigravityManager/commit/2ae2216f1ed3fa503ffd87a14cafd3921077a5c4))
## [0.6.0](https://github.com/Draculabo/AntigravityManager/compare/v0.5.0...v0.6.0) (2026-02-04)
### ✨ Features
* add cloud reset time UI ([#56](https://github.com/Draculabo/AntigravityManager/issues/56)) ([f6f8069](https://github.com/Draculabo/AntigravityManager/commit/f6f8069ce4673ae7027c6ba248daa18c8b602218))
* windows install guidance ([#63](https://github.com/Draculabo/AntigravityManager/issues/63)) ([ce71470](https://github.com/Draculabo/AntigravityManager/commit/ce7147025d37648a82f6f6b300502ef4462bfde6))
### 🐛 Bug Fixes
* correct Windows install notice path ([5bda4b1](https://github.com/Draculabo/AntigravityManager/commit/5bda4b19dfc9ce64735cd39600d41a5deefc26ee))
* **proxy:** route Claude Code CLI requests on /v1/chat/completions to Anthropic handler ([#61](https://github.com/Draculabo/AntigravityManager/issues/61)) ([476d297](https://github.com/Draculabo/AntigravityManager/commit/476d297e4b2dd3015f7ebcbb915b643730816dc6))
## [0.5.0](https://github.com/Draculabo/AntigravityManager/compare/v0.4.0...v0.5.0) (2026-01-30)
### ✨ Features
* **i18n:** add Russian localization ([#48](https://github.com/Draculabo/AntigravityManager/issues/48)) ([63956c9](https://github.com/Draculabo/AntigravityManager/commit/63956c9c2d60f829a998237abe6ade675fdb01ed))
* Implement collapsible sidebar and refined status bar UI ([#45](https://github.com/Draculabo/AntigravityManager/issues/45)) ([1265d04](https://github.com/Draculabo/AntigravityManager/commit/1265d044f69e52fba7da72fde6c47a0b85c58232))
* sentry integration ([#51](https://github.com/Draculabo/AntigravityManager/issues/51)) ([a785640](https://github.com/Draculabo/AntigravityManager/commit/a785640d7a51b65a6852383401bd7c284b716975))
## [0.4.0](https://github.com/Draculabo/AntigravityManager/compare/v0.3.5...v0.4.0) (2026-01-28)
### ✨ Features
* add system autostart and single-instance support ([ea51253](https://github.com/Draculabo/AntigravityManager/commit/ea51253d589abd537682344d3bdb684b8fc9a511))
* implement smart foreground quota refresh with debounce ([dd9e84a](https://github.com/Draculabo/AntigravityManager/commit/dd9e84a0dbefad6066193b6bd468689a755a02e3))
### 🐛 Bug Fixes
* stub nestjs optional modules for packaging ([f0eb7c6](https://github.com/Draculabo/AntigravityManager/commit/f0eb7c6b619a3ea9ea203d66f5dbce731d731e3c))
## [0.3.5](https://github.com/Draculabo/AntigravityManager/compare/v0.3.4...v0.3.5) (2026-01-26)
### 🐛 Bug Fixes
- "Check Quota Now" button not refreshing UI after polling ([#42](https://github.com/Draculabo/AntigravityManager/issues/42)) ([e959ee3](https://github.com/Draculabo/AntigravityManager/commit/e959ee346e7c26a8a4c5b7deefa5bd2452153f9d))
### 📝 Documentation
- remove beta download links from README ([5a21680](https://github.com/Draculabo/AntigravityManager/commit/5a2168030eac4ddeffa1c3b002b2de48b6a11a8f))
## [0.3.4](https://github.com/Draculabo/AntigravityManager/compare/v0.3.3...v0.3.4) (2026-01-26)
### 🐛 Bug Fixes
- **security:** add safeStorage fallback for production builds ([#38](https://github.com/Draculabo/AntigravityManager/issues/38)) ([#43](https://github.com/Draculabo/AntigravityManager/issues/43)) ([0208058](https://github.com/Draculabo/AntigravityManager/commit/02080588b764ed88a5831152a3a1249f1d077d29))
### 📝 Documentation
- update beta release link ([d5ee08d](https://github.com/Draculabo/AntigravityManager/commit/d5ee08d5a06a915a8b82f680b38e2f532105498c))
## [0.3.4-beta.1](https://github.com/Draculabo/AntigravityManager/compare/v0.3.3...v0.3.4-beta.1) (2026-01-25)
### 🐛 Bug Fixes
- **security:** add safeStorage fallback for production builds ([#38](https://github.com/Draculabo/AntigravityManager/issues/38)) ([92dc2f6](https://github.com/Draculabo/AntigravityManager/commit/92dc2f6f2169eb1a32950694387f2333ea2de682))
## [0.3.3](https://github.com/Draculabo/AntigravityManager/compare/v0.3.2...v0.3.3) (2026-01-25)
### 🐛 Bug Fixes
- accept lowercase antigravity in process detection ([0d4e2ab](https://github.com/Draculabo/AntigravityManager/commit/0d4e2ab21f37704e09ef1a67c181c48b42df1180))
### 📝 Documentation
- add beta download link to readme ([f15bb48](https://github.com/Draculabo/AntigravityManager/commit/f15bb48fdda10fda3c2382941ee0ce51204f750a))
- clean up changelog duplicate ([22265e1](https://github.com/Draculabo/AntigravityManager/commit/22265e153c9d394229aa48afdc5948044b74e842))
## [0.3.2](https://github.com/Draculabo/AntigravityManager/compare/v0.3.1...v0.3.2) (2026-01-25)
### 🐛 Bug Fixes
- handle keychain hint and suppress pgrep spam ([bd3d41a](https://github.com/Draculabo/AntigravityManager/commit/bd3d41aed17bafe9d684c5c421bad8b90afa19a8))
### 📝 Documentation
- add macOS self-signing workaround for Keychain issues ([01e3f8f](https://github.com/Draculabo/AntigravityManager/commit/01e3f8f8fd6dacc5eed214ed4b505d6d85f4bcff))
### 🔧 Continuous Integration
- setup semantic release configuration and github actions workflow ([d2945a6](https://github.com/Draculabo/AntigravityManager/commit/d2945a6e8a14d75f577716183cdff093443d9636))
- trigger publish on release published event ([6a07bc0](https://github.com/Draculabo/AntigravityManager/commit/6a07bc0a10a5ad802777e007cfd7390852119b15))
## [0.3.1] - 2026-01-25
### Bug Fixes
- Fixed startup race condition causing cloud accounts verify failure ([f0718db])
- Enabled WAL mode and force initialization on startup to resolve process resource contention ([1bce5d3])
## [0.3.0] - 2026-01-23
### New Features
- Verify Google OAuth code automatically after receipt
- Add button to open logs folder
- Add expiration warning for Google OAuth authentication
### Bug Fixes
- Fixed `state.vscdb` path on Linux to include `User/globalStorage` subdirectory (Fixed [#26](https://github.com/Draculabo/AntigravityManager/issues/26))
- Improved process detection on macOS/Linux using `find-process` to reliably identify the main application and exclude helper processes (Fixed [#27](https://github.com/Draculabo/AntigravityManager/issues/27))
- Fixed keychain access error on macOS Apple Silicon (M1/M2/M3) by adding arm64 build to CI
### Maintenance
- Add VS Code settings for auto-formatting and ESLint
## [0.2.2] - 2026-01-19
### Bug Fixes
- Fixed tray icon not appearing in production builds on Windows
- Used `extraResource` config to properly copy assets outside of ASAR package
- Added debug logging for tray icon path resolution
## [0.2.1] - 2026-01-19
### Bug Fixes
- Fixed process detection to be case-insensitive on Linux/macOS (`pgrep -xi`) ([#24](https://github.com/Draculabo/AntigravityManager/pull/24)) - Thanks [@Olbrasoft](https://github.com/Olbrasoft)!
- Fixed manager exclusion logic to prevent accidental self-termination ([#24](https://github.com/Draculabo/AntigravityManager/pull/24))
- Fixed zombie tray icons on application restart/hot reload ([#24](https://github.com/Draculabo/AntigravityManager/pull/24))
### Maintenance
- Applied Prettier formatting to entire codebase (68 files)
- Added node globals to ESLint configuration
## [0.2.0] - 2026-01-16
### New Features
- Enhanced cloudHandler to inject minimal auth state when database entry is missing, improving onboarding reliability.
- Implemented stability fixes and enhanced error handling across the application.
### Improvements
- Upgraded Electron from 32.3.3 to 37.3.1 for improved performance and security.
- Conditionally include plugins based on start command in forge.config.ts for better build flexibility.
### Bug Fixes
- Fixed "Converting circular structure to JSON" error.
### Documentation
- Added curly brace constraints for conditional statements.
- Fixed incorrect reference documentation name.
## [0.1.1] - 2026-01-11
### Bug Fixes
- Fix Antigravity visibility issue on account switch. (Fixed [#19](https://github.com/Draculabo/AntigravityManager/issues/19))
## [0.1.0] - 2026-01-10
### New Features
- LAN Connection Support: Users can now connect via Local Area Network (LAN) for improved flexibility and internal environment support.
- Antigravity Integration: Added native support and adaptation for Antigravity, enhancing overall compatibility.
- Local API Proxy: Built-in OpenAI/Anthropic compatible proxy server.
### Bug Fixes
- Reverse Proxy Issue: Resolved a critical error occurring during reverse proxy configurations. (Fixed [#11](https://github.com/Draculabo/AntigravityManager/issues/11))
## [0.0.1] - 2025-12-22
### Added
- Initial release of Antigravity Manager
- Multi-account management for Google Gemini and Claude
- Real-time quota monitoring
- Intelligent auto-switching capabilities
- Secure credential storage (AES-256-GCM)
- IDE synchronization
- Dark mode support
- System tray integration
+1
View File
@@ -0,0 +1 @@
Always refer to `@AGENTS.md`.
+49
View File
@@ -0,0 +1,49 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [INSERT EMAIL ADDRESS]. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
---
**Note**: Please replace `[INSERT EMAIL ADDRESS]` with your actual contact email if you wish to receive reports.
+185
View File
@@ -0,0 +1,185 @@
# Contributing to Antigravity Manager
First off, thank you for considering contributing to Antigravity Manager! 🎉
It's people like you that make Antigravity Manager such a great tool. We welcome contributions from everyone, whether it's a bug report, feature suggestion, documentation improvement, or code contribution.
## 📋 Table of Contents
- [Code of Conduct](#code-of-conduct)
- [Getting Started](#getting-started)
- [Development Setup](#development-setup)
- [Making Changes](#making-changes)
- [Pull Request Process](#pull-request-process)
- [Style Guidelines](#style-guidelines)
- [Reporting Bugs](#reporting-bugs)
- [Suggesting Features](#suggesting-features)
## 📜 Code of Conduct
This project and everyone participating in it is governed by our [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to the project maintainers.
## 🚀 Getting Started
1. **Fork the repository** on GitHub
2. **Clone your fork** locally:
```bash
git clone https://github.com/YOUR_USERNAME/AntigravityManager.git
cd AntigravityManager
```
3. **Add the upstream remote**:
```bash
git remote add upstream https://github.com/Draculabo/AntigravityManager.git
```
## 💻 Development Setup
### Prerequisites
- Node.js v18 or higher
- npm (comes with Node.js)
- Git
### Installation
```bash
# Install dependencies
npm install
# Start development server
npm start
```
### Available Scripts
| Command | Description |
| ---------------------- | ----------------------------------- |
| `npm start` | Start the app in development mode |
| `npm run lint` | Run ESLint to check for code issues |
| `npm run format:write` | Format code with Prettier |
| `npm run test:unit` | Run unit tests with Vitest |
| `npm run test:e2e` | Run E2E tests with Playwright |
| `npm run test:all` | Run all tests |
| `npm run type-check` | Run TypeScript type checking |
| `npm run make` | Build production packages |
## ✏️ Making Changes
1. **Create a new branch** from `main`:
```bash
git checkout -b feature/your-feature-name
# or
git checkout -b fix/your-bug-fix
```
2. **Make your changes** and commit them:
```bash
git add .
git commit -m "feat: add amazing new feature"
```
3. **Keep your branch updated**:
```bash
git fetch upstream
git rebase upstream/main
```
### Commit Message Guidelines
We follow [Conventional Commits](https://www.conventionalcommits.org/). Each commit message should be structured as follows:
```
<type>(<scope>): <description>
[optional body]
[optional footer(s)]
```
**Types:**
- `feat`: A new feature
- `fix`: A bug fix
- `docs`: Documentation only changes
- `style`: Changes that don't affect code meaning (formatting, etc.)
- `refactor`: Code change that neither fixes a bug nor adds a feature
- `perf`: Performance improvement
- `test`: Adding or correcting tests
- `chore`: Changes to build process or auxiliary tools
**Examples:**
```
feat(auth): add Google OAuth support
fix(quota): resolve quota refresh timeout issue
docs(readme): update installation instructions
```
## 🔄 Pull Request Process
1. **Update documentation** if you're changing functionality
2. **Add tests** for new features
3. **Ensure all tests pass**: `npm run test:all`
4. **Ensure code is formatted**: `npm run format:write`
5. **Ensure no lint errors**: `npm run lint`
6. **Push to your fork** and create a Pull Request
### PR Checklist
- [ ] My code follows the project's style guidelines
- [ ] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or my feature works
- [ ] New and existing unit tests pass locally with my changes
## 🎨 Style Guidelines
### TypeScript/JavaScript
- Use TypeScript for all new code
- Follow the existing code style (enforced by ESLint and Prettier)
- Use meaningful variable and function names
- Add JSDoc comments for public APIs
### React Components
- Use functional components with hooks
- Keep components small and focused
- Use TypeScript interfaces for props
- Follow the existing file structure
### CSS/Styling
- Use TailwindCSS utility classes
- Follow the design system in `components.json`
- Ensure responsive design
## 🐛 Reporting Bugs
Found a bug? Please [create an issue](https://github.com/Draculabo/AntigravityManager/issues/new?template=bug_report.md) with:
- **Clear title** describing the issue
- **Steps to reproduce** the behavior
- **Expected behavior** vs actual behavior
- **Screenshots** if applicable
- **Environment details** (OS, app version)
## 💡 Suggesting Features
Have an idea? Please [create an issue](https://github.com/Draculabo/AntigravityManager/issues/new?template=feature_request.md) with:
- **Clear description** of the feature
- **Use case** - why is this feature needed?
- **Possible implementation** ideas (optional)
## 🙏 Thank You
Your contributions help make Antigravity Manager better for everyone. Thank you for taking the time to contribute!
---
If you have any questions, feel free to open an issue or reach out to the maintainers.
+305 -81
View File
@@ -1,121 +1,339 @@
# Antigravity Manager
<p align="center">
<img src="docs/assets/logo.png" alt="Antigravity Manager" width="128" height="128" />
</p>
Antigravity Manager is a professional Electron-based application designed to manage multiple cloud AI accounts (Google Gemini / Claude) for the Antigravity application. It provides seamless account switching, real-time quota monitoring, intelligent auto-switching, and comprehensive backup management.
<h1 align="center">Antigravity Manager</h1>
## Features
<p align="center">
<strong>🚀 Professional multi-account manager for Google Gemini & Claude AI</strong>
</p>
### Core Features
<p align="center">
English | <a href="README.zh-CN.md">简体中文</a>
</p>
- **Cloud Account Pool Management**
- Add unlimited Google Gemini / Claude accounts via OAuth authorization
- Display account details: avatar, email, status, and last used time
- Real-time account status monitoring (Active, Rate Limited, Expired)
- Delete accounts from local database
<p align="center">
<a href="https://github.com/Draculabo/AntigravityManager/actions/workflows/testing.yaml">
<img src="https://github.com/Draculabo/AntigravityManager/actions/workflows/testing.yaml/badge.svg" alt="Tests" />
</a>
<a href="https://github.com/Draculabo/AntigravityManager/actions/workflows/lint.yaml">
<img src="https://github.com/Draculabo/AntigravityManager/actions/workflows/lint.yaml/badge.svg" alt="Lint" />
</a>
<a href="https://github.com/Draculabo/AntigravityManager/releases">
<img src="https://img.shields.io/github/v/release/Draculabo/AntigravityManager?style=flat-square" alt="Release" />
</a>
<a href="https://github.com/Draculabo/AntigravityManager/releases">
<img src="https://img.shields.io/github/downloads/Draculabo/AntigravityManager/total?style=flat-square&color=blue" alt="Downloads" />
</a>
<a href="https://github.com/Draculabo/AntigravityManager/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/Draculabo/AntigravityManager?style=flat-square" alt="License" />
</a>
<a href="https://github.com/Draculabo/AntigravityManager/stargazers">
<img src="https://img.shields.io/github/stars/Draculabo/AntigravityManager?style=flat-square" alt="Stars" />
</a>
</p>
- **Real-time Quota Monitoring**
- Multi-model support: Monitor quotas for `gemini-pro`, `claude-3-5-sonnet`, etc.
- Visual display with progress bars and color indicators (green/yellow/red)
- Auto & manual refresh capabilities
<p align="center">
<img src="https://img.shields.io/badge/Electron-191970?style=for-the-badge&logo=Electron&logoColor=white" alt="Electron" />
<img src="https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB" alt="React" />
<img src="https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white" alt="TypeScript" />
<img src="https://img.shields.io/badge/Tailwind_CSS-38B2AC?style=for-the-badge&logo=tailwind-css&logoColor=white" alt="TailwindCSS" />
</p>
- **Intelligent Auto-Switching**
- Unlimited pool mode: Automatically find and switch to the best backup account when current quota is low (<5%) or rate-limited
- Background monitoring: Built-in `CloudMonitorService` polls all accounts every 5 minutes
- Global toggle: Enable/disable via one-click in the UI
---
- **One-Click Account Switching**
- Inject tokens directly into IDE's `state.vscdb` database using Protobuf parsing
- Graceful process restart to apply new credentials
## 📖 Table of Contents
### Account Backup
- [Why Antigravity Manager?](#-why-antigravity-manager)
- [Features](#-features)
- [Screenshots](#-screenshots)
- [Quick Start](#-quick-start)
- [Tech Stack](#️-tech-stack)
- [Development](#-development)
- [FAQ](#-faq)
- [Contributing](#-contributing)
- [License](#-license)
- **Backup**: Capture snapshots of your current account state
- **Restore**: Fast and easy switching between multiple saved accounts
- **Management**: View, organize, and delete saved account snapshots
---
### Process Control
## ✨ Why Antigravity Manager?
- Automatically detect if Antigravity is running
- Launch Antigravity via URI protocol or executable path
- Gracefully close or force kill the Antigravity process
When using Antigravity IDE, have you ever encountered these problems?
### System Integration
- 😫 Single account quota runs out quickly, requiring frequent manual switching
- 🔄 Managing multiple Google/Claude accounts is cumbersome
- 📊 Don't know how much quota is left on the current account
- ⏰ Worried about missing quota reset times
- 🔌 Need a reliable local API proxy for development tools
- **System Tray**: Background mode with tray icon and right-click menu
- **IDE Sync**: Automatically scan and import accounts from IDE's `state.vscdb`
- **Batch Operations**: Batch refresh and delete multiple accounts
**Antigravity Manager** is here to solve these problems! It's a professional Electron desktop app that helps you:
### Security Hardening
- ✅ **Unlimited Account Pool** - Add any number of Google Gemini / Claude accounts
- ✅ **Smart Auto-Switching** - Automatically switch to the next available account when quota is low or rate-limited
- ✅ **Real-time Monitoring** - Visualize quota usage for all accounts
- ✅ **Local API Proxy** - Built-in OpenAI/Anthropic compatible proxy server
- ✅ **Secure Encryption** - AES-256-GCM encryption for sensitive data
- **Key Management**: Uses OS native credential manager (Windows Credential Manager / macOS Keychain) via `keytar` to securely store Master Key (AES-256)
- **Data Encryption**: All sensitive data (`token_json`, `quota_json`) encrypted with `AES-256-GCM` before storage
- **Auto Migration**: Automatic detection and migration of legacy plaintext data on startup
---
### Other Features
## 🎯 Features
- **Internationalization**: Multi-language support (English / 中文)
- **Modern UI**: Built with React, TailwindCSS, and Shadcn UI for a clean and responsive experience
<table>
<tr>
<td width="50%">
<h3>☁️ Cloud Account Pool</h3>
<ul>
<li>Add unlimited Google Gemini / Claude accounts via OAuth</li>
<li>Display avatar, email, status, and last used time</li>
<li>Real-time status monitoring (Active, Rate Limited, Expired)</li>
</ul>
</td>
<td width="50%">
<h3>📊 Real-time Quota Monitoring</h3>
<ul>
<li>Multi-model support: gemini-pro, claude-3-5-sonnet, etc.</li>
<li>Visual progress bars with color indicators</li>
<li>Auto & manual refresh capabilities</li>
</ul>
</td>
</tr>
<tr>
<td width="50%">
<h3>🔄 Intelligent Auto-Switching</h3>
<ul>
<li>Unlimited pool mode with smart backup selection</li>
<li>Auto-switch when quota < 5% or rate-limited</li>
<li>Background monitoring every 5 minutes</li>
</ul>
</td>
<td width="50%">
<h3>🔐 Security First</h3>
<ul>
<li>AES-256-GCM encryption for sensitive data</li>
<li>OS native credential manager integration</li>
<li>Auto migration of legacy plaintext data</li>
</ul>
</td>
</tr>
<tr>
<td width="50%">
<h3>💾 Account Backup</h3>
<ul>
<li>Capture snapshots of account state</li>
<li>Fast switching between saved accounts</li>
<li>View, organize, and delete snapshots</li>
</ul>
</td>
<td width="50%">
<h3>⚙️ Process Control</h3>
<ul>
<li>Auto-detect if Antigravity is running</li>
<li>Launch via URI protocol or executable</li>
<li>Graceful close or force kill</li>
</ul>
</td>
</tr>
<tr>
<td width="50%">
<h3>🔌 Local API Proxy</h3>
<ul>
<li>OpenAI & Anthropic API compatible</li>
<li>Configurable port and request timeout</li>
<li>Model mapping (e.g. Claude → Gemini)</li>
</ul>
</td>
<td width="50%">
<h3>🛠️ Developer Tools</h3>
<ul>
<li>Built-in cURL & Python code generation</li>
<li>Visual service status monitoring</li>
<li>One-click API Key regeneration</li>
</ul>
</td>
</tr>
</table>
## Tech Stack
### Additional Features
- **Core**: [Electron](https://www.electronjs.org/), [React](https://react.dev/), [TypeScript](https://www.typescriptlang.org/)
- **Build Tool**: [Vite](https://vitejs.dev/)
- **Styling**: [TailwindCSS](https://tailwindcss.com/)
- **State Management**: [TanStack Query](https://tanstack.com/query/latest)
- **Routing**: [TanStack Router](https://tanstack.com/router/latest)
- **Database**: [Better-SQLite3](https://github.com/WiseLibs/better-sqlite3)
- **Testing**: [Vitest](https://vitest.dev/), [Playwright](https://playwright.dev/)
- **🖥️ System Tray** - Background mode with tray icon and right-click menu
- **🔗 IDE Sync** - Automatically scan and import accounts from IDE's `state.vscdb`
- **📦 Batch Operations** - Batch refresh and delete multiple accounts
- **🌏 Internationalization** - Multi-language support (English / 中文)
- **🎨 Modern UI** - Built with React, TailwindCSS, and Shadcn UI
## Getting Started
---
### Prerequisites
## 📸 Screenshots
- Node.js (v18 or higher recommended)
<p align="center">
<img src="docs/assets/screenshot-main.png" alt="Main Interface" width="80%" />
</p>
<p align="center">
<img src="docs/assets/screenshot-proxy.png" alt="Proxy Interface" width="48%" />
<img src="docs/assets/screenshot-setting.png" alt="Settings Interface" width="48%" />
</p>
---
## � Quick Start
### Download
Download the latest release for your platform from the [Releases](https://github.com/Draculabo/AntigravityManager/releases) page.
| Platform | Download |
|----------|----------|
| Windows (x64/ARM64) | [.exe installer](https://github.com/Draculabo/AntigravityManager/releases/latest) |
| macOS | [.dmg installer](https://github.com/Draculabo/AntigravityManager/releases/latest) |
| Linux | [.deb / .rpm](https://github.com/Draculabo/AntigravityManager/releases/latest) |
### Build from Source
#### Prerequisites
- Node.js v18 or higher
- npm or yarn
### Installation
1. Clone the repository:
```bash
git clone <repository-url>
cd AntigravityManager
```
2. Install dependencies:
```bash
npm install
```
### Development
To start the application in development mode:
#### Steps
```bash
# Clone the repository
git clone https://github.com/Draculabo/AntigravityManager.git
cd AntigravityManager
# Install dependencies
npm install
# Start development
npm start
```
This will launch the Electron app with hot-reloading enabled.
### Building
To create a distributable package for your platform:
```bash
# Build for production
npm run make
```
The output will be available in the `out` directory.
---
### Testing
## �🛠️ Tech Stack
- Run unit tests: `npm run test:unit`
- Run E2E tests: `npm run test:e2e`
- Run all tests: `npm run test:all`
| Category | Technologies |
|----------|-------------|
| **Core** | [Electron](https://www.electronjs.org/), [React](https://react.dev/), [TypeScript](https://www.typescriptlang.org/) |
| **Build Tool** | [Vite](https://vitejs.dev/) |
| **Styling** | [TailwindCSS](https://tailwindcss.com/), [Shadcn UI](https://ui.shadcn.com/) |
| **State** | [TanStack Query](https://tanstack.com/query/latest), [TanStack Router](https://tanstack.com/router/latest) |
| **Database** | [Better-SQLite3](https://github.com/WiseLibs/better-sqlite3) |
| **Testing** | [Vitest](https://vitest.dev/), [Playwright](https://playwright.dev/) |
## License
---
## � Development
### Available Scripts
| Command | Description |
|---------|-------------|
| `npm start` | Start the app in development mode |
| `npm run lint` | Run ESLint to check for code issues |
| `npm run format:write` | Format code with Prettier |
| `npm run test:unit` | Run unit tests with Vitest |
| `npm run test:e2e` | Run E2E tests with Playwright |
| `npm run test:all` | Run all tests |
| `npm run type-check` | Run TypeScript type checking |
| `npm run make` | Build production packages |
### Project Structure
```
AntigravityManager/
├── src/
│ ├── main.ts # Electron main process
│ ├── preload.ts # Preload script
│ ├── renderer/ # React renderer process
│ ├── ipc/ # IPC communication handlers
│ └── server/ # Built-in server
├── docs/ # Documentation and assets
└── .github/ # GitHub configuration
```
---
## ❓ FAQ
<details>
<summary><b>Q: The app won't start?</b></summary>
Please check:
1. Make sure all dependencies are installed: `npm install`
2. Check if Node.js version is >= 18
3. Try deleting `node_modules` and reinstalling
</details>
<details>
<summary><b>Q: Account login failed?</b></summary>
1. Ensure network connection is working
2. Try clearing app data and logging in again
3. Check if the account is restricted by Google/Claude
</details>
<details>
<summary><b>Q: macOS shows Keychain/Credential error and OAuth cannot be saved?</b></summary>
This is a common macOS security behavior, usually when the app is unsigned or run directly from Downloads.
This is a **temporary workaround** for personal use:
1. Move the app to `/Applications`
2. Run the following commands in Terminal (repeat after every update)
```plaintext
sudo xattr -dr com.apple.quarantine "/Applications/Antigravity Manager 2.app"
codesign --force --deep --sign - "/Applications/Antigravity Manager 2.app"
```
Reopen the app and allow Keychain access if prompted.
</details>
<details>
<summary><b>Q: How to report issues or suggestions?</b></summary>
Please submit issues or suggestions via [GitHub Issues](https://github.com/Draculabo/AntigravityManager/issues).
</details>
---
## 🌟 Star History
<a href="https://github.com/Draculabo/AntigravityManager/stargazers">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=Draculabo/AntigravityManager&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=Draculabo/AntigravityManager&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=Draculabo/AntigravityManager&type=Date" />
</picture>
</a>
---
## 🤝 Contributing
Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details.
We follow the [Contributor Covenant](CODE_OF_CONDUCT.md) Code of Conduct.
---
## 📄 License
[CC BY-NC-SA 4.0](LICENSE)
## Disclaimer
---
## ⚠️ Disclaimer
> [!WARNING]
> **For Educational Purposes Only**
@@ -123,3 +341,9 @@ The output will be available in the `out` directory.
> This project is intended solely for educational and research purposes. It is provided "as-is" without any warranty. **Commercial use is strictly prohibited.**
>
> By using this software, you agree that you will not use it for any commercial purposes, and you are solely responsible for ensuring your use complies with all applicable laws and regulations. The authors and contributors are not responsible for any misuse or damages arising from the use of this software.
---
<p align="center">
If this project helps you, please give it a ⭐ Star!
</p>
+349
View File
@@ -0,0 +1,349 @@
<p align="center">
<img src="docs/assets/logo.png" alt="Antigravity Manager" width="128" height="128" />
</p>
<h1 align="center">Antigravity Manager</h1>
<p align="center">
<strong>🚀 专业的 Google Gemini & Claude AI 多账号管理器</strong>
</p>
<p align="center">
<a href="README.md">English</a> | 简体中文
</p>
<p align="center">
<a href="https://github.com/Draculabo/AntigravityManager/actions/workflows/testing.yaml">
<img src="https://github.com/Draculabo/AntigravityManager/actions/workflows/testing.yaml/badge.svg" alt="Tests" />
</a>
<a href="https://github.com/Draculabo/AntigravityManager/actions/workflows/lint.yaml">
<img src="https://github.com/Draculabo/AntigravityManager/actions/workflows/lint.yaml/badge.svg" alt="Lint" />
</a>
<a href="https://github.com/Draculabo/AntigravityManager/releases">
<img src="https://img.shields.io/github/v/release/Draculabo/AntigravityManager?style=flat-square" alt="Release" />
</a>
<a href="https://github.com/Draculabo/AntigravityManager/releases">
<img src="https://img.shields.io/github/downloads/Draculabo/AntigravityManager/total?style=flat-square&color=blue" alt="Downloads" />
</a>
<a href="https://github.com/Draculabo/AntigravityManager/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/Draculabo/AntigravityManager?style=flat-square" alt="License" />
</a>
<a href="https://github.com/Draculabo/AntigravityManager/stargazers">
<img src="https://img.shields.io/github/stars/Draculabo/AntigravityManager?style=flat-square" alt="Stars" />
</a>
</p>
<p align="center">
<img src="https://img.shields.io/badge/Electron-191970?style=for-the-badge&logo=Electron&logoColor=white" alt="Electron" />
<img src="https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB" alt="React" />
<img src="https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white" alt="TypeScript" />
<img src="https://img.shields.io/badge/Tailwind_CSS-38B2AC?style=for-the-badge&logo=tailwind-css&logoColor=white" alt="TailwindCSS" />
</p>
---
## 📖 目录
- [为什么选择 Antigravity Manager?](#-为什么选择-antigravity-manager)
- [功能特性](#-功能特性)
- [截图预览](#-截图预览)
- [快速开始](#-快速开始)
- [技术栈](#️-技术栈)
- [开发指南](#-开发指南)
- [常见问题](#-常见问题)
- [贡献指南](#-贡献指南)
- [许可证](#-许可证)
---
## ✨ 为什么选择 Antigravity Manager?
在使用 Antigravity IDE 时,你是否遇到过这些问题?
- 😫 单个账号额度很快用完,需要频繁手动切换
- 🔄 管理多个 Google/Claude 账号非常麻烦
- 📊 不知道当前账号还剩多少额度
- ⏰ 担心错过额度重置时间
- 🔌 需要可靠的本地 API 代理用于开发工具
**Antigravity Manager** 就是为解决这些问题而生的!它是一个专业的 Electron 桌面应用,帮助你:
- ✅ **无限账号池** - 添加任意数量的 Google Gemini / Claude 账号
- ✅ **智能自动切换** - 额度不足或被限速时自动切换到下一个可用账号
- ✅ **实时监控** - 可视化显示所有账号的额度使用情况
- ✅ **本地 API 代理** - 内置兼容 OpenAI/Anthropic 协议的代理服务器
- ✅ **安全加密** - AES-256-GCM 加密存储敏感信息
---
## 🎯 功能特性
<table>
<tr>
<td width="50%">
<h3>☁️ 云账号池管理</h3>
<ul>
<li>通过 OAuth 添加无限 Google Gemini / Claude 账号</li>
<li>显示头像、邮箱、状态和最后使用时间</li>
<li>实时状态监控(活跃、限速、过期)</li>
</ul>
</td>
<td width="50%">
<h3>📊 实时额度监控</h3>
<ul>
<li>多模型支持:gemini-pro、claude-3-5-sonnet 等</li>
<li>可视化进度条配色指示器</li>
<li>自动刷新与手动刷新</li>
</ul>
</td>
</tr>
<tr>
<td width="50%">
<h3>🔄 智能自动切换</h3>
<ul>
<li>无限池模式,智能选择备用账号</li>
<li>额度低于 5% 或被限速时自动切换</li>
<li>后台每 5 分钟自动监控</li>
</ul>
</td>
<td width="50%">
<h3>🔐 安全优先</h3>
<ul>
<li>AES-256-GCM 加密敏感数据</li>
<li>集成操作系统原生凭证管理器</li>
<li>自动迁移旧版明文数据</li>
</ul>
</td>
</tr>
<tr>
<td width="50%">
<h3>💾 账号备份</h3>
<ul>
<li>捕获账号状态快照</li>
<li>在已保存的账号间快速切换</li>
<li>查看、整理和删除快照</li>
</ul>
</td>
<td width="50%">
<h3>⚙️ 进程控制</h3>
<ul>
<li>自动检测 Antigravity 是否运行</li>
<li>通过 URI 协议或可执行文件启动</li>
<li>优雅关闭或强制终止</li>
</ul>
</td>
</tr>
<tr>
<td width="50%">
<h3>🔌 本地 API 代理</h3>
<ul>
<li>兼容 OpenAI & Anthropic API 协议</li>
<li>可配置端口和请求超时</li>
<li>模型映射(如:Claude → Gemini)</li>
</ul>
</td>
<td width="50%">
<h3>🛠️ 开发者工具</h3>
<ul>
<li>内置 cURL & Python 代码生成</li>
<li>可视化服务状态监控</li>
<li>一键重新生成 API Key</li>
</ul>
</td>
</tr>
</table>
### 更多功能
- **🖥️ 系统托盘** - 后台运行,托盘图标和右键菜单
- **🔗 IDE 同步** - 自动扫描导入 IDE 的 `state.vscdb` 账号
- **📦 批量操作** - 批量刷新和删除多个账号
- **🌏 国际化** - 多语言支持(English / 中文)
- **🎨 现代 UI** - 基于 React、TailwindCSS 和 Shadcn UI 构建
---
## 📸 截图预览
<p align="center">
<img src="docs/assets/screenshot-main.png" alt="主界面" width="80%" />
</p>
<p align="center">
<img src="docs/assets/screenshot-proxy.png" alt="代理界面" width="48%" />
<img src="docs/assets/screenshot-setting.png" alt="设置界面" width="48%" />
</p>
---
## 🚀 快速开始
### 下载安装
从 [Releases](https://github.com/Draculabo/AntigravityManager/releases) 页面下载适合你平台的最新版本。
| 平台 | 下载链接 |
| ------- | ------------------------------------------------------------------------------ |
| Windows (x64/ARM64) | [.exe 安装包](https://github.com/Draculabo/AntigravityManager/releases/latest) |
| macOS | [.dmg 安装包](https://github.com/Draculabo/AntigravityManager/releases/latest) |
| Linux | [.deb / .rpm](https://github.com/Draculabo/AntigravityManager/releases/latest) |
### 从源码构建
#### 前置要求
- Node.js v18 或更高版本
- npm 或 yarn
#### 步骤
```bash
# 克隆仓库
git clone https://github.com/Draculabo/AntigravityManager.git
cd AntigravityManager
# 安装依赖
npm install
# 启动开发模式
npm start
# 构建生产版本
npm run make
```
---
## 🛠️ 技术栈
| 类别 | 技术 |
| ------------ | ------------------------------------------------------------------------------------------------------------------- |
| **核心** | [Electron](https://www.electronjs.org/)、[React](https://react.dev/)、[TypeScript](https://www.typescriptlang.org/) |
| **构建工具** | [Vite](https://vitejs.dev/) |
| **样式** | [TailwindCSS](https://tailwindcss.com/)、[Shadcn UI](https://ui.shadcn.com/) |
| **状态管理** | [TanStack Query](https://tanstack.com/query/latest)、[TanStack Router](https://tanstack.com/router/latest) |
| **数据库** | [Better-SQLite3](https://github.com/WiseLibs/better-sqlite3) |
| **测试** | [Vitest](https://vitest.dev/)、[Playwright](https://playwright.dev/) |
---
## 💻 开发指南
### 可用脚本
| 命令 | 描述 |
| ---------------------- | ------------------------ |
| `npm start` | 启动开发模式 |
| `npm run lint` | 运行 ESLint 检查 |
| `npm run format:write` | 使用 Prettier 格式化代码 |
| `npm run test:unit` | 运行单元测试 |
| `npm run test:e2e` | 运行 E2E 测试 |
| `npm run test:all` | 运行所有测试 |
| `npm run type-check` | TypeScript 类型检查 |
| `npm run make` | 构建生产包 |
### 项目结构
```
AntigravityManager/
├── src/
│ ├── main.ts # Electron 主进程
│ ├── preload.ts # 预加载脚本
│ ├── renderer/ # React 渲染进程
│ ├── ipc/ # IPC 通信处理
│ └── server/ # 内置服务器
├── docs/ # 文档和资源
└── .github/ # GitHub 配置
```
---
## ❓ 常见问题
<details>
<summary><b>Q: 程序无法启动怎么办?</b></summary>
请检查:
1. 确保已安装所有依赖:`npm install`
2. 检查 Node.js 版本是否 >= 18
3. 尝试删除 `node_modules` 后重新安装
</details>
<details>
<summary><b>Q: 账号登录失败?</b></summary>
1. 确保网络连接正常
2. 尝试清除应用数据后重新登录
3. 检查账号是否被 Google/Claude 限制
</details>
<details>
<summary><b>Q: macOS 上提示 Keychain/凭据管理不可用,OAuth 无法保存?</b></summary>
这是 macOS 安全机制导致的常见问题,通常出现在未签名的应用或从 Downloads 直接运行的场景。
这是**临时方案**,仅适合个人设备:
1. 将应用移动到 `/Applications`
2. 打开终端执行以下命令(每次更新后都需要重新执行)
```plaintext
sudo xattr -dr com.apple.quarantine "/Applications/Antigravity Manager 2.app"
codesign --force --deep --sign - "/Applications/Antigravity Manager 2.app"
```
重新打开应用后,Keychain 应会提示授权(可选“始终允许”)。
</details>
<details>
<summary><b>Q: 如何反馈问题或建议?</b></summary>
请通过 [GitHub Issues](https://github.com/Draculabo/AntigravityManager/issues) 提交问题或建议。
</details>
---
## 🌟 Star History
<a href="https://github.com/Draculabo/AntigravityManager/stargazers">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=Draculabo/AntigravityManager&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=Draculabo/AntigravityManager&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=Draculabo/AntigravityManager&type=Date" />
</picture>
</a>
---
## 🤝 贡献指南
欢迎贡献代码!请阅读 [CONTRIBUTING.md](CONTRIBUTING.md) 了解详情。
我们遵循 [Contributor Covenant](CODE_OF_CONDUCT.md) 行为准则。
---
## 📄 许可证
[CC BY-NC-SA 4.0](LICENSE)
---
## ⚠️ 免责声明
> [!WARNING]
> **仅供学习研究使用**
>
> 本项目仅供教育和研究目的,按"原样"提供,不提供任何保证。**严禁商业使用。**
>
> 使用本软件即表示您同意不会将其用于任何商业目的,并自行负责确保您的使用符合所有适用法律法规。作者和贡献者对因使用本软件而产生的任何滥用或损害不承担责任。
---
<p align="center">
如果这个项目对你有帮助,请给一个 ⭐ Star 支持!
</p>
+93
View File
@@ -0,0 +1,93 @@
# Security Policy
## 🔒 Security Features
Antigravity Manager takes security seriously. Here are the security measures we've implemented:
### Encryption
- **AES-256-GCM** encryption for all sensitive data (`token_json`, `quota_json`)
- Unique initialization vectors (IV) for each encryption operation
- Authenticated encryption to prevent tampering
### Credential Storage
- **Windows**: Windows Credential Manager
- **macOS**: macOS Keychain
- **Linux**: Secret Service API / libsecret
Master keys are stored in the OS native credential manager via `keytar`, never in plain text files.
### Data Protection
- Sensitive tokens are encrypted before storage in SQLite database
- Automatic migration of legacy plaintext data on application startup
- No sensitive data is logged or transmitted to external services
## 📋 Supported Versions
| Version | Supported |
| -------- | ------------------ |
| Latest | :white_check_mark: |
| < Latest | :x: |
We only provide security updates for the latest version. Please ensure you're running the most recent release.
## 🚨 Reporting a Vulnerability
We take all security vulnerabilities seriously. Thank you for helping to improve the security of Antigravity Manager.
### How to Report
**Please DO NOT report security vulnerabilities through public GitHub issues.**
Instead, please report them via one of the following methods:
1. **GitHub Security Advisories**: Use our [private vulnerability reporting](https://github.com/Draculabo/AntigravityManager/security/advisories/new)
2. **Email**: Send details to the project maintainer (see GitHub profile for contact)
### What to Include
Please include the following information in your report:
- Type of vulnerability (e.g., buffer overflow, SQL injection, XSS)
- Full paths of source file(s) related to the vulnerability
- Location of the affected source code (tag/branch/commit or direct URL)
- Step-by-step instructions to reproduce the issue
- Proof-of-concept or exploit code (if possible)
- Impact of the issue and how an attacker might exploit it
### Response Timeline
- **Initial Response**: Within 48 hours
- **Status Update**: Within 5 business days
- **Resolution Timeline**: Depends on complexity, typically within 30 days
### What to Expect
1. **Acknowledgment**: We'll confirm receipt of your report
2. **Investigation**: We'll investigate and validate the vulnerability
3. **Fix Development**: We'll develop and test a fix
4. **Disclosure**: We'll coordinate disclosure timing with you
5. **Credit**: With your permission, we'll credit you in the release notes
## 🛡️ Security Best Practices for Users
To ensure the security of your data:
1. **Keep Updated**: Always use the latest version of Antigravity Manager
2. **System Security**: Keep your operating system updated
3. **Secure Environment**: Use the application on trusted devices only
4. **Backup**: Regularly backup your account data using the built-in backup feature
5. **Review Permissions**: Only grant necessary OAuth permissions
## 📜 Disclosure Policy
When we receive a security bug report, we will:
1. Confirm the problem and determine affected versions
2. Audit code to find any similar problems
3. Prepare fixes for all supported versions
4. Release new versions as soon as possible
Thank you for helping keep Antigravity Manager and its users safe!
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

+70 -60
View File
@@ -1,86 +1,96 @@
# 云端账号管理与智能切换 (Cloud Account Features)
# Cloud Account Management and Intelligent Switching
本文档详细介绍了 Antigravity Manager 中实现的云端 AI 账号管理与切换功能。该功能允许用户管理多个 Google 账号,监控 API 配额,并一键无缝切换 IDE 的当前活动账号。
This document explains the cloud AI account management and switching capabilities in Antigravity Manager. The feature allows users to manage multiple Google accounts, monitor API quotas, and seamlessly switch the active IDE account with one click.
## 1. 核心功能
## 1. Core Features
### 1.1 账号池管理
- **添加账号**:支持通过 Google OAuth 授权码添加账号。
- **列表展示**:展示所有已添加的账号,包括头像、邮箱、最后使用时间。
- **状态监控**:实时显示账号状态(Active, Rate Limited, Expired)以及是否为当前活动账号。
- **删除账号**:支持从本地数据库中移除账号。
### 1.1 Account Pool Management
### 1.2 配额实时监控
- **多模型支持**:分别监控 `gemini-pro`, `claude-3-5-sonnet` 等模型的配额使用情况。
- **可视化展示**:通过进度条和颜色(绿/黄/红)直观展示剩余配额百分比。
- **自动/手动刷新**:支持手动刷新配额,系统也会在切换前自动检查。
- **Add accounts**: Add accounts via Google OAuth authorization code.
- **List view**: Display all added accounts, including avatar, email, and last used time.
- **Status monitoring**: Show real-time account status (Active, Rate Limited, Expired) and whether the account is currently active.
- **Delete accounts**: Remove accounts from the local database.
### 1.4 智能自动切换 (Intelligent Auto-Switching)
- **无限池模式**:当检测到当前账号配额不足(<5%)或被限流时,系统会自动寻找最佳备用账号并切换。
- **后台监控**:内置 `CloudMonitorService`, 默认每 5 分钟轮询一次所有账号的 Quota 状态。
- **全局开关**:用户可在界面上一键开启或关闭此功能。
### 1.2 Real-Time Quota Monitoring
## 2. 技术实现
- **Multi-model support**: Track quota usage for models such as `gemini-pro`, `claude-3-5-sonnet`, and more.
- **Visual indicators**: Use progress bars and color states (green/yellow/red) to show remaining quota percentage.
- **Auto/manual refresh**: Support manual quota refresh; the system also checks quota automatically before switching.
### 2.1 数据库设计 (`cloud_accounts.db`)
使用 SQLite 存储账号信息,独立于 IDE 的本地数据库。
### 1.4 Intelligent Auto-Switching
- **Unlimited pool mode**: When the current account quota is low (`<5%`) or rate-limited, the system automatically finds and switches to the best backup account.
- **Background monitoring**: Built-in `CloudMonitorService` polls quota status for all accounts every 5 minutes by default.
- **Global toggle**: Users can enable or disable this feature with one click in the UI.
## 2. Technical Implementation
### 2.1 Database Design (`cloud_accounts.db`)
Account information is stored in SQLite, independent from the IDE's local database.
```sql
CREATE TABLE accounts (
id TEXT PRIMARY KEY, -- UUID
provider TEXT NOT NULL, -- 'google' | 'anthropic'
email TEXT NOT NULL, -- 邮箱
name TEXT, -- 显示名称
avatar_url TEXT, -- 头像 URL
token_json TEXT NOT NULL, -- OAuth Token (JSON)
quota_json TEXT, -- 配额数据 (JSON)
created_at INTEGER NOT NULL, -- 创建时间戳
last_used INTEGER NOT NULL, -- 最后使用时间戳
status TEXT DEFAULT 'active', -- 账号状态
is_active INTEGER DEFAULT 0 -- 是否为当前 IDE 活动账号
email TEXT NOT NULL, -- Email
name TEXT, -- Display name
avatar_url TEXT, -- Avatar URL
token_json TEXT NOT NULL, -- OAuth token (JSON)
quota_json TEXT, -- Quota data (JSON)
created_at INTEGER NOT NULL, -- Created timestamp
last_used INTEGER NOT NULL, -- Last-used timestamp
status TEXT DEFAULT 'active', -- Account status
is_active INTEGER DEFAULT 0 -- Whether this is the currently active IDE account
);
-- 全局配置表
-- Global settings table
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
```
### 2.2 IPC 接口设计
后端通过 `src/ipc/cloud` 提供以下 oRPC 接口:
### 2.2 IPC Interface Design
- `addGoogleAccount(authCode: string)`: 交换 Token 并保存账号。
- `listCloudAccounts()`: 获取账号列表。
- `refreshAccountQuota(accountId: string)`: 刷新指定账号的配额和 Token。
- `switchCloudAccount(accountId: string)`: 执行账号切换流程。
- `deleteCloudAccount(accountId: string)`: 删除账号。
- `getAutoSwitchEnabled()` / `setAutoSwitchEnabled(enabled)`: 获取/设置自动切换开关。
- `forcePollCloudMonitor()`: 手动触发后台轮询。
The backend exposes the following oRPC interfaces via `src/ipc/cloud`:
### 2.3 Token 注入原理 (`ProtobufUtils`)
IDE 将鉴权信息存储在 `state.vscdb` 的 `ItemTable` 表中,key 为 `jetskiStateSync.agentManagerInitState`。
value 是一个 Base64 编码的 Protobuf 二进制数据。
- `addGoogleAccount(authCode: string)`: Exchange token and save account.
- `listCloudAccounts()`: Get account list.
- `refreshAccountQuota(accountId: string)`: Refresh quota and token for a specific account.
- `switchCloudAccount(accountId: string)`: Run the account switch workflow.
- `deleteCloudAccount(accountId: string)`: Delete an account.
- `getAutoSwitchEnabled()` / `setAutoSwitchEnabled(enabled)`: Get/set auto-switch state.
- `forcePollCloudMonitor()`: Manually trigger background polling.
我们实现了 `src/utils/protobuf.ts` 工具类,能够:
1. **解码**:解析 Varint 和 Length-Delimited 字段。
2. **修改**:定位并移除旧的 Field 6 (OAuthTokenInfo)。
3. **重构**:根据新的 Access Token 和 Refresh Token 构建新的 Field 6 并插入。
### 2.3 Token Injection Mechanism (`ProtobufUtils`)
### 2.4 切换流程
1. **Token 检查**:检查目标账号 Token 是否临期,如需刷新则自动刷新。
2. **停止进程**:优雅关闭 Antigravity 进程。
3. **注入 Token**:调用 `CloudAccountRepo.injectCloudToken` 修改 IDE 数据库。
4. **更新状态**:将目标账号标记为 `is_active = 1`,其他账号设为 0。
5. **重启进程**:重新启动 IDE,使其加载新的凭证。
The IDE stores authentication data in the `ItemTable` table of `state.vscdb`, under the key `jetskiStateSync.agentManagerInitState`.
The value is Base64-encoded Protobuf binary.
### 2.5 自动切换逻辑 (`AutoSwitchService`)
1. **监控**:`CloudMonitorService` 定时(5分钟)轮询所有账号配额。
2. **判断**:如果当前活动账号的所有关键模型配额低于 5%,或状态为 `rate_limited`。
3. **择优**:从账号池中筛选状态为 `active` 且配额充足的账号,按配额剩余量排序。
4. **执行**:自动调用 `switchCloudAccount` 完成切换,并通知用户。
We implemented `src/utils/protobuf.ts`, which can:
### 2.6 安全加固 (Security Hardening)
- **密钥管理**:使用操作系统原生凭据管理器(Windows Credential Manager / macOS Keychain)通过 `keytar` 库安全存储 Master Key(AES-256)。
- **数据加密**:所有敏感数据(`token_json`, `quota_json`)在存入 SQLite 数据库前均使用 `AES-256-GCM` 算法加密。
- **自动迁移**:应用启动时会自动检测并迁移旧的明文数据,确保安全性平滑升级。
1. **Decode**: Parse Varint and length-delimited fields.
2. **Modify**: Locate and remove old Field 6 (`OAuthTokenInfo`).
3. **Rebuild**: Build a new Field 6 using new access/refresh tokens and insert it.
### 2.4 Switching Workflow
1. **Token check**: Check whether the target account token is near expiration; refresh automatically if needed.
2. **Stop process**: Gracefully stop the Antigravity process.
3. **Inject token**: Call `CloudAccountRepo.injectCloudToken` to update the IDE database.
4. **Update state**: Mark target account as `is_active = 1`, set others to `0`.
5. **Restart process**: Restart the IDE so it loads new credentials.
### 2.5 Auto-Switch Logic (`AutoSwitchService`)
1. **Monitor**: `CloudMonitorService` polls quotas for all accounts every 5 minutes.
2. **Evaluate**: If all key model quotas of the active account are below `5%`, or status is `rate_limited`.
3. **Select**: Filter accounts with `active` status and sufficient quota, then sort by remaining quota.
4. **Execute**: Automatically call `switchCloudAccount` and notify the user.
### 2.6 Security Hardening
- **Key management**: Use native OS credential stores (Windows Credential Manager / macOS Keychain) via `keytar` to securely store the AES-256 master key.
- **Data encryption**: Encrypt all sensitive fields (`token_json`, `quota_json`) with `AES-256-GCM` before writing to SQLite.
- **Auto migration**: Automatically detect and migrate legacy plaintext data at startup to ensure a smooth security upgrade.
+24 -14
View File
@@ -1,28 +1,38 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginReact from "eslint-plugin-react";
import eslintPluginPrettierRecommended from "eslint-config-prettier";
import reactHooks from "eslint-plugin-react-hooks";
import path from "node:path";
import { includeIgnoreFile } from "@eslint/compat";
import { fileURLToPath } from "node:url";
import { defineConfig } from "eslint/config";
import globals from 'globals';
import pluginJs from '@eslint/js';
import tseslint from 'typescript-eslint';
import pluginReact from 'eslint-plugin-react';
import eslintPluginPrettierRecommended from 'eslint-config-prettier';
import reactHooks from 'eslint-plugin-react-hooks';
import path from 'node:path';
import { includeIgnoreFile } from '@eslint/compat';
import { fileURLToPath } from 'node:url';
import { defineConfig } from 'eslint/config';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const prettierIgnorePath = path.resolve(__dirname, ".prettierignore");
const prettierIgnorePath = path.resolve(__dirname, '.prettierignore');
/** @type {import('eslint').Linter.Config[]} */
export default defineConfig([
includeIgnoreFile(prettierIgnorePath),
{
files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
files: ['**/*.{js,mjs,cjs,ts,jsx,tsx}'],
},
{ languageOptions: { globals: globals.browser } },
{ languageOptions: { globals: { ...globals.browser, ...globals.node } } },
pluginJs.configs.recommended,
pluginReact.configs.flat["jsx-runtime"],
pluginReact.configs.flat['jsx-runtime'],
reactHooks.configs.flat.recommended,
eslintPluginPrettierRecommended,
...tseslint.configs.recommended,
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-empty-object-type': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'no-empty-pattern': 'off',
'prefer-const': 'off',
},
},
]);
+450 -20
View File
@@ -1,16 +1,203 @@
import type { ForgeConfig } from '@electron-forge/shared-types';
import { MakerSquirrel } from '@electron-forge/maker-squirrel';
import { MakerZIP } from '@electron-forge/maker-zip';
import { MakerDeb } from '@electron-forge/maker-deb';
import { MakerRpm } from '@electron-forge/maker-rpm';
import { VitePlugin } from '@electron-forge/plugin-vite';
import { FusesPlugin } from '@electron-forge/plugin-fuses';
import crypto from 'crypto';
import { FuseV1Options, FuseVersion } from '@electron/fuses';
import type {
HookFunction,
HookFunctionErrorCallback,
TargetArch,
TargetPlatform,
} from '@electron/packager';
import { MakerDeb } from '@electron-forge/maker-deb';
import { MakerDMG } from '@electron-forge/maker-dmg';
import { MakerRpm } from '@electron-forge/maker-rpm';
import { MakerSquirrel } from '@electron-forge/maker-squirrel';
import { MakerWix } from '@electron-forge/maker-wix';
import { MakerZIP } from '@electron-forge/maker-zip';
import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives';
import { FusesPlugin } from '@electron-forge/plugin-fuses';
import { VitePlugin } from '@electron-forge/plugin-vite';
import type { ForgeConfig } from '@electron-forge/shared-types';
import MakerAppImage from '@pengx17/electron-forge-maker-appimage';
import setLanguages from 'electron-packager-languages';
import * as fs from 'fs';
import * as path from 'path';
import { stringify as yamlStringify } from 'yaml';
const nativeModules = ['better-sqlite3', 'keytar', 'bindings', 'file-uri-to-path'];
const ResolvedMakerAppImage = MakerAppImage;
const keepLanguages = new Set(['en', 'en-US', 'zh-CN', 'ru']);
const windowsExecutableName = 'antigravity-manager';
const isStartCommand = process.argv.some((arg) => arg.includes('start'));
const artifactRegex = /.*\.(?:exe|dmg|AppImage|zip|deb|rpm|msi)$/;
const platformNamesMap: Record<string, string> = {
darwin: 'macos',
linux: 'linux',
win32: 'windows',
};
const ymlBaseNameMap: Record<string, string> = {
darwin: 'latest-mac',
linux: 'latest-linux',
win32: 'latest',
};
const ignorePatterns = [
/^\/\.git/,
/^\/\.github/,
/^\/\.vscode/,
/^\/\.idea/,
/^\/openspec/,
/^\/docs?/,
/^\/scripts?/,
/^\/tests?/,
/^\/mocks?/,
/^\/src/,
/^\/node_modules\/\.cache/,
];
const setLanguagesHook = setLanguages([...keepLanguages.values()]);
const packagerAfterCopy: HookFunction[] = [
(
buildPath: string,
electronVersion: string,
platform: TargetPlatform,
arch: TargetArch,
callback: HookFunctionErrorCallback,
) => {
if (platform !== 'win32') {
callback();
return;
}
setLanguagesHook(buildPath, electronVersion, platform, arch, callback);
},
];
function normalizeArtifactName(value?: string) {
if (!value) {
return 'app';
}
return value
.trim()
.replace(/\s+/g, '.')
.replace(/[^a-zA-Z0-9.]/g, '')
.replace(/\.+/g, '.');
}
function isSquirrelArtifact(artifactPath: string) {
const fileName = path.basename(artifactPath);
if (fileName === 'RELEASES') {
return true;
}
return artifactPath.endsWith('.nupkg');
}
function mapArchName(arch: string, mapping: Record<string, string>) {
return mapping[arch] || arch;
}
function getArtifactFileName({
baseName,
version,
arch,
extension,
}: {
baseName: string;
version: string;
arch: string;
extension: string;
}) {
if (extension === '.rpm') {
return `${baseName}-${version}-1.${arch}${extension}`;
}
if (extension === '.deb') {
return `${baseName}_${version}_${arch}${extension}`;
}
if (extension === '.AppImage') {
return `${baseName}_${version}_${arch}${extension}`;
}
if (extension === '.dmg') {
return `${baseName}_${version}_${arch}${extension}`;
}
if (extension === '.exe') {
return `${baseName}_${version}_${arch}-setup${extension}`;
}
if (extension === '.msi') {
return `${baseName}_${version}_${arch}_en-US${extension}`;
}
if (extension === '.zip') {
return `${baseName}_${version}_${arch}${extension}`;
}
return `${baseName}_${version}_${arch}${extension}`;
}
function getUpdateYmlFileName(platform: string, arch: string) {
const baseName = ymlBaseNameMap[platform];
if (!baseName) {
return null;
}
if (platform === 'darwin') {
return arch === 'universal' ? `${baseName}.yml` : `${baseName}-${arch}.yml`;
}
if (platform === 'linux') {
return arch === 'x64' ? `${baseName}.yml` : `${baseName}-${arch}.yml`;
}
if (platform === 'win32') {
return arch === 'x64' ? `${baseName}.yml` : `${baseName}-${arch}.yml`;
}
return null;
}
function getChecksumArchLabel(platform: string, arch: string) {
if (platform === 'linux') {
return mapArchName(arch, { x64: 'amd64', arm64: 'aarch64' });
}
if (platform === 'darwin') {
return mapArchName(arch, { x64: 'x64', arm64: 'arm64', universal: 'universal' });
}
if (platform === 'win32') {
return mapArchName(arch, { x64: 'x64', arm64: 'arm64' });
}
return arch;
}
const appImageMaker = new ResolvedMakerAppImage({
config: {
icons: [
{
file: 'images/32x32.png',
size: 32,
},
{
file: 'images/64x64.png',
size: 64,
},
{
file: 'images/128x128.png',
size: 128,
},
{
file: 'images/[email protected]',
size: 256,
},
],
},
});
appImageMaker.name = '@pengx17/electron-forge-maker-appimage';
const config: ForgeConfig = {
packagerConfig: {
@@ -18,8 +205,12 @@ const config: ForgeConfig = {
unpack: '**/{better-sqlite3,keytar}/**/*',
},
name: 'Antigravity Manager',
executableName: 'antigravity-manager',
executableName: windowsExecutableName,
icon: 'images/icon', // Electron Forge automatically adds .icns/.ico
extraResource: ['src/assets'], // Copy assets folder to resources/assets
afterCopy: packagerAfterCopy,
ignore: ignorePatterns,
prune: true,
},
rebuildConfig: {},
hooks: {
@@ -45,9 +236,245 @@ const config: ForgeConfig = {
for (const moduleName of nativeModules) {
copyModuleRecursive(moduleName);
}
// Copy assets to resources folder
const assetsSrc = path.join(process.cwd(), 'src', 'assets');
const assetsDest = path.join(buildPath, 'resources', 'assets');
if (fs.existsSync(assetsSrc)) {
if (!fs.existsSync(assetsDest)) {
fs.mkdirSync(assetsDest, { recursive: true });
}
fs.cpSync(assetsSrc, assetsDest, { recursive: true });
console.log(`Copied assets from ${assetsSrc} to ${assetsDest}`);
} else {
console.warn(`Assets directory not found: ${assetsSrc}`);
}
},
postMake: async (_config, makeResults) => {
if (!makeResults?.length) {
return makeResults;
}
const ymlByTarget = new Map<
string,
{
basePath: string;
fileName: string;
yml: {
version?: string;
files: {
url: string;
sha512: string;
size: number;
}[];
releaseDate?: string;
};
}
>();
const checksumByTarget = new Map<
string,
{
basePath: string;
fileName: string;
lines: string[];
}
>();
makeResults = makeResults.map((result) => {
const productName = normalizeArtifactName(
result.packageJSON.productName || result.packageJSON.name,
);
const platformName = platformNamesMap[result.platform] || result.platform;
const version = result.packageJSON.version;
const platformKey = result.platform;
const archKey = result.arch;
const updateFileName = getUpdateYmlFileName(platformKey, archKey);
const updateKey = updateFileName ? `${platformKey}-${archKey}` : null;
const checksumKey = `${platformKey}-${archKey}`;
const checksumArchLabel = getChecksumArchLabel(platformKey, archKey);
const checksumFileName = `sha256sums-${platformName}-${checksumArchLabel}.txt`;
if (!checksumByTarget.has(checksumKey)) {
checksumByTarget.set(checksumKey, {
basePath: '',
fileName: checksumFileName,
lines: [],
});
}
if (updateFileName && updateKey && !ymlByTarget.has(updateKey)) {
ymlByTarget.set(updateKey, {
basePath: '',
fileName: updateFileName,
yml: {
version,
files: [],
},
});
}
const updateState = updateKey ? ymlByTarget.get(updateKey)! : null;
const checksumState = checksumByTarget.get(checksumKey)!;
result.artifacts = result.artifacts
.map((artifact) => {
if (!artifact) {
return null;
}
if (isSquirrelArtifact(artifact)) {
return artifact;
}
if (!artifactRegex.test(artifact)) {
return artifact;
}
if (!checksumState.basePath) {
checksumState.basePath = path.dirname(artifact);
}
if (updateState && !updateState.basePath) {
updateState.basePath = path.dirname(artifact);
}
const extension = path.extname(artifact);
let archLabel = archKey;
if (platformKey === 'linux' && extension === '.rpm') {
archLabel = mapArchName(archKey, { x64: 'x86_64', arm64: 'aarch64' });
} else if (platformKey === 'linux' && extension === '.deb') {
archLabel = mapArchName(archKey, { x64: 'amd64', arm64: 'arm64' });
} else if (platformKey === 'linux' && extension === '.AppImage') {
archLabel = mapArchName(archKey, { x64: 'amd64', arm64: 'aarch64' });
} else if (platformKey === 'darwin') {
archLabel = mapArchName(archKey, {
x64: 'x64',
arm64: 'arm64',
universal: 'universal',
});
} else if (platformKey === 'win32') {
archLabel = mapArchName(archKey, { x64: 'x64', arm64: 'arm64' });
}
const newArtifact = `${path.dirname(artifact)}/${getArtifactFileName({
baseName: productName,
version,
arch: archLabel,
extension,
})}`;
if (newArtifact !== artifact) {
fs.renameSync(artifact, newArtifact);
}
try {
const fileData = fs.readFileSync(newArtifact);
const hash = crypto.createHash('sha512').update(fileData).digest('base64');
const sha256 = crypto.createHash('sha256').update(fileData).digest('hex');
const { size } = fs.statSync(newArtifact);
if (updateState) {
updateState.yml.files.push({
url: path.basename(newArtifact),
sha512: hash,
size,
});
}
checksumState.lines.push(`${sha256} ${path.basename(newArtifact)}`);
} catch {
console.error(`Failed to hash ${newArtifact}`);
}
return newArtifact;
})
.filter((artifact) => artifact !== null);
return result;
});
const releaseDate = new Date().toISOString();
for (const [updateKey, updateState] of ymlByTarget.entries()) {
if (!updateState.basePath) {
continue;
}
updateState.yml.releaseDate = releaseDate;
const ymlPath = path.join(updateState.basePath, updateState.fileName);
fs.writeFileSync(ymlPath, yamlStringify(updateState.yml));
const [platform, arch] = updateKey.split('-');
const sampleResult = makeResults.find(
(result) => result.platform === platform && result.arch === arch,
);
if (!sampleResult) {
continue;
}
makeResults.push({
artifacts: [ymlPath],
platform: sampleResult.platform,
arch: sampleResult.arch,
packageJSON: sampleResult.packageJSON,
});
}
for (const [checksumKey, checksumState] of checksumByTarget.entries()) {
if (!checksumState.basePath || checksumState.lines.length === 0) {
continue;
}
const checksumPath = path.join(checksumState.basePath, checksumState.fileName);
fs.writeFileSync(checksumPath, `${checksumState.lines.join('\n')}\n`);
const [platform, arch] = checksumKey.split('-');
const sampleResult = makeResults.find(
(result) => result.platform === platform && result.arch === arch,
);
if (!sampleResult) {
continue;
}
makeResults.push({
artifacts: [checksumPath],
platform: sampleResult.platform,
arch: sampleResult.arch,
packageJSON: sampleResult.packageJSON,
});
}
return makeResults;
},
},
makers: [new MakerSquirrel({}), new MakerZIP({}, ['darwin']), new MakerRpm({}), new MakerDeb({})],
makers: [
new MakerSquirrel({
setupIcon: 'images/icon.ico',
iconUrl:
'https://raw.githubusercontent.com/Draculabo/AntigravityManager/main/images/icon.ico',
}),
...(process.platform === 'win32' && process.arch === 'x64'
? [
new MakerWix({
language: 1033,
icon: path.join(process.cwd(), 'images', 'icon.ico'),
exe: `${windowsExecutableName}.exe`,
ui: { chooseDirectory: true },
}),
]
: []),
new MakerDMG(
{
overwrite: true,
icon: 'images/icon.icns',
iconSize: 160,
},
['darwin'],
),
new MakerZIP({}, ['darwin']),
appImageMaker,
new MakerRpm({}),
new MakerDeb({}),
],
publishers: [
{
/*
@@ -66,7 +493,6 @@ const config: ForgeConfig = {
},
],
plugins: [
new AutoUnpackNativesPlugin({}),
new VitePlugin({
build: [
{
@@ -87,16 +513,20 @@ const config: ForgeConfig = {
},
],
}),
new FusesPlugin({
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true,
}),
...(!isStartCommand
? [
new AutoUnpackNativesPlugin({}),
new FusesPlugin({
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true,
}),
]
: []),
],
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 889 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 659 B

@@ -0,0 +1,36 @@
## Context
The existing Electron Forge packaging pipeline needs Linux arm64, macOS universal, and Windows MSI outputs, plus consistent artifact naming and SHA-256 checksum outputs for cross-platform release parity.
## Goals / Non-Goals
- Goals:
- Cover Linux arm64 artifacts (`deb`/`rpm`/`AppImage`)
- Provide macOS `x64`/`arm64`/`universal` dmg artifacts
- Add MSI artifacts for Windows
- Include architecture markers in artifact names and output SHA-256 checksums
- Non-Goals:
- Change existing update logic (`latest*.yml` remains unchanged)
- Modify runtime application behavior
## Decisions
- Decision: Build Linux arm64 on a dedicated arm64 runner, without cross-compilation.
- Decision: Build macOS universal via `--arch=universal` or by merging app bundles before dmg packaging.
- Decision: Generate Windows MSI with the official Forge maker (Wix/MSI) in parallel with Squirrel.
- Decision: Use `productName + version + arch` naming; Linux uses `amd64/aarch64`, macOS/Windows use `x64/arm64/universal`.
## Alternatives Considered
- x64-only artifacts: rejected because it does not meet user requirements.
- universal-only dmg: rejected due to weaker debugging/rollback capability and lack of architecture distinction.
## Risks / Trade-offs
- Linux arm64 builds may be affected by native dependency compilation (for example, `better-sqlite3`, `keytar`).
- Windows MSI requires extra tooling (Wix), which may increase CI duration and failure rate.
- The universal dmg pipeline requires validation for signing and update-flow consistency.
## Migration Plan
1) Introduce CI multi-arch jobs with naming and checksum outputs.
2) Add MSI maker and universal dmg packaging.
3) Stabilize the workflow after release validation.
## Open Questions
- Should artifact naming strictly match sample format (underscore vs hyphen)?
- Should checksum output be separate `.sha256` files or a single consolidated manifest?
@@ -0,0 +1,20 @@
# Change: Add Multi-Architecture and Multi-Format Release Artifacts
## Why
Current release artifacts only cover part of the required architectures and formats. This does not meet demand for Linux arm64, macOS universal, and Windows MSI, and it lacks consistent naming and SHA-256 checksum outputs aligned with distribution requirements.
## What Changes
- Add Linux arm64 (`aarch64`) builds producing `.deb`, `.rpm`, and `.AppImage`.
- Add `macOS universal.dmg` (while keeping x64/arm64 dmg outputs).
- Add Windows MSI artifacts in parallel with existing Squirrel `.exe`.
- Standardize artifact naming and architecture identifiers, and generate SHA-256 checksum files.
## Impact
- Affected specs: `release-packaging`
- Affected code:
- `E:\project\Crack\.github\workflows\publish.yaml`
- `E:\project\Crack\forge.config.ts`
- `E:\project\Crack\package.json`
@@ -0,0 +1,41 @@
## ADDED Requirements
### Requirement: Linux Multi-Architecture Release Artifacts
The system SHALL produce Linux x64 and arm64 release artifacts, including `.deb`, `.rpm`, and `.AppImage`.
#### Scenario: Linux x64 artifact generation
- **WHEN** building a release on Linux x64
- **THEN** release assets include x64 `.deb`, `.rpm`, and `.AppImage` files
#### Scenario: Linux arm64 artifact generation
- **WHEN** building a release on Linux arm64
- **THEN** release assets include arm64 `.deb`, `.rpm`, and `.AppImage` files
### Requirement: macOS Multi-Architecture and Universal dmg
The system SHALL produce macOS x64, arm64, and universal dmg artifacts in release builds.
#### Scenario: macOS x64/arm64 dmg generation
- **WHEN** building releases for macOS x64 and arm64
- **THEN** release assets include dmg files for each corresponding architecture
#### Scenario: macOS universal dmg generation
- **WHEN** running a universal build
- **THEN** release assets include a universal dmg
### Requirement: Parallel Windows MSI and Squirrel Artifacts
The system SHALL generate both Squirrel installer artifacts and MSI artifacts in Windows release builds.
#### Scenario: Windows MSI generation
- **WHEN** building a release on Windows x64
- **THEN** release assets include both `.exe` installer and `.msi` artifacts
### Requirement: Artifact Naming and SHA-256 Output
The system SHALL output SHA-256 checksums for all release artifacts and use a unified naming convention that includes architecture markers.
#### Scenario: Artifact naming includes version and architecture
- **WHEN** generating release artifacts
- **THEN** artifact names include `productName`, `version`, and `arch` identifiers
#### Scenario: SHA-256 checksum output
- **WHEN** release artifact generation finishes
- **THEN** release assets include SHA-256 checksums for corresponding artifacts
@@ -0,0 +1,7 @@
## 1. Implementation
- [x] 1.1 Update the release workflow matrix to add Linux arm64 builds and split build steps by architecture.
- [x] 1.2 Add macOS universal build flow (merge app bundles before generating universal dmg).
- [x] 1.3 Introduce Windows MSI maker and required dependencies, and generate MSI in the Windows job.
- [x] 1.4 Extend `postMake` artifact renaming and architecture mapping logic to generate SHA-256 checksum files.
- [ ] 1.5 Update release attachments and artifact inventory documentation (if needed).
- [ ] 1.6 Verify CI outputs for Linux arm64, macOS universal, and Windows MSI.
@@ -0,0 +1,35 @@
## Context
The stability of account injection and detection differs across platforms, with risks concentrated in:
- DB lock conflicts causing unstable writes.
- Version-detection failures causing incorrect injection-path routing.
- Compatibility boundaries introduced by OS behavior differences.
## Goals / Non-Goals
- Goals:
- Improve cross-platform write and switching stability.
- Define clear priority between version detection and capability probing.
- Improve observability and troubleshooting efficiency.
- Non-Goals:
- Extend support to non-default runtime configuration path parsing.
- Introduce new business strategies.
## Decisions
- Decision: Transactionalize the write flow to reduce partial-success risk.
- Decision: Add DB busy retry and `busy_timeout`.
- Decision: On version-detection failure, use `package.json` as Windows fallback source.
- Decision: Give capability probing higher priority than version detection; prefer new format when a new-format key is detected.
## Risks / Trade-offs
- A stricter write flow may add minor latency.
- Running capability probing and version detection together increases implementation complexity.
## Migration Plan
1. Complete transactional writes and retry mechanisms.
2. Add version-detection cache and fallback.
3. Introduce strategy-based injection and capability probing.
4. Add corresponding tests and regression verification.
@@ -0,0 +1,35 @@
# Change: Cross-Platform Stability and Robustness Improvements (P0+P1+P2)
## Why
- Account injection and detection across platforms are vulnerable to DB locks, version-detection failures, and OS-level differences.
- Stability, diagnosability, and maintainability must improve without changing business semantics.
## What Changes
- Make the write flow atomic (transaction-wrapped).
- Add DB lock retry and `busy_timeout`.
- Add cache and lower-noise logging for version detection.
- Add a more robust Windows fallback for version detection (read from `package.json`).
- Introduce strategy-based injection routing (new/legacy/dual-write).
- Add capability probing: prefer new format when new-format keys are detected.
## Impact
- Affected specs: `ide-auth-compat`
- Affected code:
- `src/ipc/database/cloudHandler.ts`
- `src/ipc/database/handler.ts`
- `src/utils/antigravityVersion.ts`
- `src/utils/paths.ts` (this stage covers default runtime configuration path only)
- `src/types/db.ts`
## Constraints
- Do not change existing business semantics; only improve stability and structure.
- This stage only covers the default runtime configuration path.
## Risks
- Transaction and retry logic may change when errors surface.
- Capability probing may conflict with version detection unless priority is clearly defined.
@@ -0,0 +1,42 @@
## ADDED Requirements
### Requirement: Atomic Injection Writes
The system SHALL use transactions when writing IDE state to ensure atomic updates.
#### Scenario: Injection write consistency
- **WHEN** injecting new/legacy token formats
- **THEN** related keys are committed in a single transaction to avoid partial writes
### Requirement: DB Lock Retry
The system SHALL perform short backoff retries on SQLITE_BUSY or SQLITE_LOCKED.
#### Scenario: Successful DB lock retry
- **WHEN** a write encounters a lock
- **THEN** the system retries for a limited number of attempts and records logs
### Requirement: Version Detection Cache
The system SHALL cache IDE version-detection results in-process to reduce repeated system calls.
#### Scenario: Version cache hit
- **WHEN** IDE version is requested multiple times
- **THEN** the system call is executed only once
### Requirement: Windows Version Detection Fallback
The system SHALL read `package.json` as a fallback source when Windows version detection fails.
#### Scenario: Windows version detection failure
- **WHEN** PowerShell fails to read version
- **THEN** version is obtained by falling back to `package.json`
### Requirement: Strategy-Based Injection
The system SHALL use a strategy pattern to split new/legacy/dual-write injection paths for clearer structure.
#### Scenario: Strategy selection
- **WHEN** version-based and capability-based routing differ
- **THEN** the system selects the corresponding injection implementation via strategy objects
### Requirement: Prefer New Format via Capability Probing
The system SHALL prefer new-format injection when new-format keys are detected.
#### Scenario: New-format key exists
- **WHEN** `antigravityUnifiedStateSync.oauthToken` exists
- **THEN** injection uses new format with higher priority
@@ -0,0 +1,17 @@
## 1. Scope and Planning Confirmation
- [ ] 1.1 Review `proposal.md`.
- [ ] 1.2 Finalize priority: capability probing vs version detection.
- [ ] 1.3 Confirm this stage covers default runtime configuration path only.
## 2. Implementation
- [x] 2.1 Make write flow transactional (atomic).
- [x] 2.2 Add DB lock retry and `busy_timeout`.
- [x] 2.3 Add version-detection cache and lower-noise logs.
- [x] 2.4 Add Windows fallback for version detection (`package.json`).
- [x] 2.5 Split injection path by strategy pattern.
- [x] 2.6 Add capability probing: prefer new format when new key is detected.
## 3. Verification
- [x] 3.1 Simulate SQLITE_BUSY/LOCKED retry behavior.
- [x] 3.2 Verify fallback path for version-detection failure.
- [x] 3.3 Verify capability-probing priority behavior.
@@ -1,30 +1,30 @@
# 设计:多语言支持
# Design: Multilingual Support
## 架构
## Architecture
应用使用 `react-i18next` 进行国际化,并配合 `i18next-browser-languagedetector` 进行语言检测。配置集中在 `src/localization/i18n.ts` 中。
The application uses `react-i18next` for i18n and `i18next-browser-languagedetector` for language detection. Configuration is centralized in `src/localization/i18n.ts`.
### 语言资源
### Language Resources
翻译资源将作为 JSON 对象构建在 `i18n.ts` 中(如果太大则分文件)。
我们将在 `resources` 对象中添加 `zh-CN` 键。
Translation resources are built as JSON objects in `i18n.ts` (or split into files if they grow large).
We add a `zh-CN` key in the `resources` object.
### 默认语言策略
### Default Language Strategy
我们将引入 `LanguageDetector` 插件。
`i18n.use(LanguageDetector).init({...})` 将被配置为首先检查 `localStorage`,然后检查 `navigator`(浏览器/系统语言)。
`fallbackLng` 将保持为 `'en'`,作为最后的兜底。
We introduce the `LanguageDetector` plugin.
`i18n.use(LanguageDetector).init({...})` is configured to check `localStorage` first, then `navigator` (browser/system language).
`fallbackLng` remains `'en'` as the final fallback.
### 键映射
### Key Mapping
我们将把现有的英文字符串映射到语义键(例如 `home.title`, `settings.theme`)。
Existing English strings are mapped to semantic keys (for example, `home.title`, `settings.theme`).
## 数据流
## Data Flow
1. 应用启动。
2. `i18next` 初始化,通过 `LanguageDetector` 确定语言:
* 检查 `localStorage` 是否有保存的偏好。
* 如果无,检查 `navigator.language`。
* 如果检测到的语言受支持(`en` 或 `zh-CN`),则使用该语言。
* 否则,使用 `fallbackLng` (`en`)。
3. 组件使用 `useTranslation` hook 根据当前语言获取字符串。
1. App starts.
2. `i18next` initializes and determines language via `LanguageDetector`:
* Check whether `localStorage` has a saved preference.
* If not, check `navigator.language`.
* If detected language is supported (`en` or `zh-CN`), use it.
* Otherwise, use `fallbackLng` (`en`).
3. Components use `useTranslation` to resolve strings for the active language.
@@ -1,41 +1,41 @@
# 提案:应用本地化(自动检测系统语言)
# Proposal: App Localization (Automatic System Language Detection)
**变更 ID:** `localize-app-auto-detect`
**Change ID:** `localize-app-auto-detect`
## 摘要
## Summary
为 Antigravity Manager 应用添加多语言支持,并根据用户的操作系统语言自动设置默认语言。如果系统语言不受支持,则回退到英语。这包括更新 i18n 配置,添加中文翻译资源,并集成语言检测插件。
Add multilingual support to Antigravity Manager and automatically set the default UI language based on the user's OS language. If the OS language is unsupported, fall back to English. This includes updating i18n configuration, adding Chinese translation resources, and integrating a language detection plugin.
## 动机
## Motivation
为了提供最佳的用户体验,应用应该能够适应用户的首选语言环境。自动检测系统语言可以减少用户的配置步骤,提供更自然的初次使用体验。
To provide a better user experience, the app should adapt to users' preferred language environment. Automatically detecting system language reduces setup effort and improves first-run experience.
## 建议变更
## Proposed Changes
1. **集成语言检测:** 安装并配置 `i18next-browser-languagedetector` 以自动检测用户语言。
2. **更新 i18n 配置:** 修改 `src/localization/i18n.ts`,启用语言检测,并设置适当的回退语言(英语)。
3. **添加中文资源:** 为 `zh-CN` 创建一个新的翻译资源文件,包含所有应用字符串。
4. **更新语言操作:** 确保 `src/actions/language.ts` 正确处理自动检测和手动覆盖的逻辑。
5. **UI 更新:** 确保所有 UI 组件使用翻译键而不是硬编码字符串。
1. **Integrate language detection:** Install and configure `i18next-browser-languagedetector` to auto-detect user language.
2. **Update i18n config:** Modify `src/localization/i18n.ts` to enable language detection and define a fallback language (English).
3. **Add Chinese resources:** Create `zh-CN` translation resources covering all app strings.
4. **Update language actions:** Ensure `src/actions/language.ts` correctly handles auto-detection and manual override.
5. **UI updates:** Ensure all UI components use translation keys instead of hardcoded text.
## 考虑的替代方案
## Alternatives Considered
* **强制默认中文:** 已拒绝,为了适应更广泛的国际用户群体,并尊重用户的系统设置。
* **强制默认英语:** 已拒绝,同上。
* **Force default Chinese:** Rejected, to support a broader international user base and respect system settings.
* **Force default English:** Rejected, same reason.
## 风险
## Risks
* **检测失败:** 在某些特殊环境(如 Linux 某些发行版)下,语言检测可能不准确,需要确保回退机制可靠。
* **Detection failure:** In certain environments (for example, some Linux distros), language detection may be inaccurate. Fallback behavior must remain reliable.
## 验收标准
## Acceptance Criteria
1. **自动检测:**
* 在系统语言设置为中文(简体)的环境下启动应用,应用界面默认显示为中文。
* 在系统语言设置为英语或其他非中文语言的环境下启动应用,应用界面默认显示为英语。
2. **手动切换:**
* 用户可以在设置页面将语言从中文切换到英语,界面应立即更新,无需重启。
* 用户可以在设置页面将语言从英语切换到中文,界面应立即更新,无需重启。
3. **持久化:**
* 用户手动更改语言后,重启应用,应用应保持用户选择的语言,而不是重新检测系统语言。
4. **完整性:**
* 应用的所有可见文本(包括主页、设置页、状态栏、弹窗提示)都应正确显示对应的语言,没有遗漏的硬编码字符串。
1. **Automatic detection:**
* When OS language is Simplified Chinese, app defaults to Chinese on first launch.
* When OS language is English or another non-Chinese language, app defaults to English on first launch.
2. **Manual switching:**
* Users can switch from Chinese to English in Settings, and UI updates immediately without restart.
* Users can switch from English to Chinese in Settings, and UI updates immediately without restart.
3. **Persistence:**
* After users manually change language, app restart preserves the selected language instead of re-detecting.
4. **Completeness:**
* All visible strings (home page, settings page, status bar, popups) render correctly in the selected language with no leftover hardcoded text.
@@ -1,59 +1,59 @@
# 规格:本地化
# Spec: Localization
## ADDED Requirements
### Requirement: 自动语言检测
### Requirement: Automatic Language Detection
应用在首次启动时 MUST 自动检测用户的系统语言并设置相应的界面语言。
On first launch, the application MUST automatically detect the user's system language and set the corresponding UI language.
#### Scenario: 系统语言为中文
#### Scenario: System language is Chinese
Given 用户首次打开应用
And 用户的系统语言设置为中文 (zh-CN)
And 本地存储中没有保存语言偏好
When 应用加载时
Then 界面应该显示为简体中文 (`zh-CN`)
Given the user opens the app for the first time
And the user's system language is set to Chinese (`zh-CN`)
And no language preference is saved in local storage
When the app loads
Then the UI should be displayed in Simplified Chinese (`zh-CN`)
#### Scenario: 系统语言为其他(非中文)
#### Scenario: System language is non-Chinese
Given 用户首次打开应用
And 用户的系统语言设置为法语 (fr)
And 本地存储中没有保存语言偏好
When 应用加载时
Then 界面应该回退显示为英语 (`en`)
Given the user opens the app for the first time
And the user's system language is set to French (`fr`)
And no language preference is saved in local storage
When the app loads
Then the UI should fall back to English (`en`)
### Requirement: 语言切换
### Requirement: Language Switching
用户 MUST 能够在支持的语言之间切换,且手动选择 MUST 覆盖自动检测。
Users MUST be able to switch between supported languages, and manual selection MUST override automatic detection.
#### Scenario: 切换语言
#### Scenario: Switch language
Given 用户在设置页面
When 用户从语言下拉菜单中选择 "English"
Then 界面应该立即切换为英语
And 偏好应该保存到本地存储
Given the user is on the settings page
When the user selects "English" from the language dropdown
Then the UI should switch to English immediately
And the preference should be saved to local storage
### Requirement: 持久化
### Requirement: Persistence
用户的手动语言偏好 MUST 在会话之间持久化,并优先于系统语言。
The user's manual language preference MUST persist across sessions and take precedence over system language.
#### Scenario: 持久化语言偏好
#### Scenario: Persist language preference
Given 用户已手动选择 "English" 作为首选语言
And 用户的系统语言设置为中文
When 用户重启应用
Then 界面应该显示为英语(尊重用户选择)
Given the user has manually selected "English" as preferred language
And the user's system language is Chinese
When the user restarts the app
Then the UI should display English (respecting user preference)
## MODIFIED Requirements
### Requirement: i18n 配置更新
### Requirement: Update i18n Configuration
i18n 配置 MUST 更新以支持语言检测插件。
i18n configuration MUST be updated to support the language detection plugin.
#### Scenario: i18n 配置
#### Scenario: i18n configuration
`src/localization/i18n.ts` 文件必须更新为:
`src/localization/i18n.ts` must be updated to:
1. 集成 `i18next-browser-languagedetector`。
2. 配置检测顺序:`['localStorage', 'navigator']`。
3. 包含 `zh-CN` 资源对象。
1. Integrate `i18next-browser-languagedetector`.
2. Configure detection order as `['localStorage', 'navigator']`.
3. Include `zh-CN` resource objects.
@@ -1,10 +1,10 @@
# 任务:应用本地化(自动检测系统语言)
# Tasks: App Localization (Automatic System Language Detection)
* \[x] 安装 `i18next-browser-languagedetector` 依赖。 <!-- id: install-dependency -->
* \[x] 更新 `src/localization/i18n.ts` 以配置语言检测器并包含 `zh-CN` 资源。 <!-- id: update-i18n-config -->
* \[x] 验证 `src/actions/language.ts` 逻辑,确保与自动检测兼容。 <!-- id: verify-language-actions -->
* \[x] 更新 `src/components/StatusBar.tsx` 以使用翻译。 <!-- id: localize-statusbar -->
* \[x] 更新 `src/components/AccountCard.tsx` 以使用翻译。 <!-- id: localize-accountcard -->
* \[x] 更新 `src/pages/index.tsx`(主页)以使用翻译。 <!-- id: localize-homepage -->
* \[x] 更新 `src/pages/settings.tsx` 以使用翻译并添加语言切换器。 <!-- id: localize-settings -->
* \[x] 运行应用验证语言检测和手动切换功能。 <!-- id: verify-localization -->
* \[x] Install `i18next-browser-languagedetector`. <!-- id: install-dependency -->
* \[x] Update `src/localization/i18n.ts` to configure language detection and include `zh-CN` resources. <!-- id: update-i18n-config -->
* \[x] Verify `src/actions/language.ts` logic is compatible with auto-detection. <!-- id: verify-language-actions -->
* \[x] Update `src/components/StatusBar.tsx` to use translations. <!-- id: localize-statusbar -->
* \[x] Update `src/components/AccountCard.tsx` to use translations. <!-- id: localize-accountcard -->
* \[x] Update `src/pages/index.tsx` (home page) to use translations. <!-- id: localize-homepage -->
* \[x] Update `src/pages/settings.tsx` to use translations and add a language switcher. <!-- id: localize-settings -->
* \[x] Run the app to verify language detection and manual switching. <!-- id: verify-localization -->
@@ -0,0 +1,35 @@
## Context
Current SQLite access mainly relies on native `better-sqlite3` APIs, without unified typing and runtime validation strategy. This raises maintenance cost and regression risk. Introducing Drizzle ORM aims to improve type safety and maintainability while preserving existing behavior and compatibility.
## Goals / Non-Goals
- Goals:
- Provide structured schema and type inference
- Unify DB access and validation patterns, reducing `any/as`
- Preserve WAL, `busy_timeout`, transactions, and compatibility logic
- Non-Goals:
- Do not change existing database schema
- Do not introduce a new database engine or remote storage
## Decisions
- Decision: Adopt Drizzle ORM while keeping `better-sqlite3` as the driver.
- Decision: Keep runtime validation and compatibility logic for external IDE DB (`ItemTable`).
- Decision: Migrate in order: internal DB first, external DB second.
## Alternatives Considered
- Continue with `better-sqlite3 + Zod`: limited type-safety gains and higher long-term maintenance cost.
- Prisma: heavyweight footprint and higher Electron integration cost.
## Risks / Trade-offs
- Packaging compatibility risk (Electron + native module ABI): requires cross-platform validation.
- Temporary complexity increase while dual stacks coexist during migration.
- Drizzle provides static typing only; runtime validation is still required to avoid dirty data.
## Migration Plan
1. Define Drizzle schema (`accounts`/`settings`/`ItemTable`).
2. Migrate `cloudHandler.ts` (internal DB).
3. Migrate `database/handler.ts` (external IDE DB).
4. Update unit tests and mocks.
5. Run cross-platform smoke verification.
## Open Questions
- Loose compatibility mode is confirmed; stronger runtime schema is not introduced for now.
@@ -0,0 +1,17 @@
# Change: Refactor SQLite Access with Drizzle ORM
## Why
Current `better-sqlite3` usage relies heavily on `any/as`, lacks unified typing and runtime validation, and increases long-term maintenance cost. Introducing Drizzle ORM provides structured schema definitions and more consistent type constraints.
## What Changes
- Introduce Drizzle ORM and define schema for the internal database.
- Incrementally migrate read/write paths in `cloudHandler` and `database/handler`.
- Preserve existing WAL, `busy_timeout`, and transaction behavior to avoid regressions.
- Keep runtime validation and compatibility logic for the external IDE DB (`ItemTable`).
## Impact
- Affected code: `src/ipc/database/cloudHandler.ts`, `src/ipc/database/handler.ts`, tests, and DB utility layer
- Affected behavior: data-access typing and runtime validation approach changes (regression validation required)
@@ -0,0 +1,21 @@
## ADDED Requirements
### Requirement: Use Drizzle ORM for Internal Database
The system SHALL use Drizzle ORM to access the internal database (`cloud_accounts.db`) and provide type constraints through schema.
#### Scenario: Read cloud account list
- **WHEN** the system reads the `accounts` table
- **THEN** returned data SHALL match Drizzle schema types
### Requirement: Keep Compatibility and Validation for External IDE Database
The system SHALL preserve compatibility logic and runtime validation when accessing the external IDE database (`ItemTable`).
#### Scenario: Read ItemTable key
- **WHEN** reading a specific key from `ItemTable`
- **THEN** data that fails validation SHALL be logged as warning and skipped
### Requirement: Maintain Database Connection Stability
The system SHALL preserve existing WAL, `busy_timeout`, and transaction strategies to avoid migration regressions.
#### Scenario: Transactional write consistency
- **WHEN** executing write operations
- **THEN** related updates SHALL be committed within the same transaction
@@ -0,0 +1,19 @@
## 1. Scope and Migration Confirmation
- [x] 1.1 Confirm migration scope (internal DB vs external IDE DB).
- [x] 1.2 Confirm Drizzle driver choice (keep `better-sqlite3`).
- [x] 1.3 Confirm no changes to existing database schema.
## 2. Design and Preparation
- [x] 2.1 Build Drizzle schema (`accounts`/`settings`/`ItemTable`).
- [x] 2.2 Define connection initialization and pragma strategy (WAL, `busy_timeout`).
- [x] 2.3 Define runtime validation strategy (external IDE DB, loose compatibility).
## 3. Implementation
- [x] 3.1 Migrate read/write paths in `cloudHandler.ts`.
- [x] 3.2 Migrate read/write paths in `database/handler.ts`.
- [x] 3.3 Preserve existing transaction and retry semantics.
## 4. Testing and Verification
- [x] 4.1 Update unit tests and mocks.
- [ ] 4.2 Run regression checks for account backup/restore and injection flow.
- [ ] 4.3 Complete cross-platform smoke verification (Win/macOS/Linux).
@@ -0,0 +1,40 @@
# Device Fingerprint Capability Acceptance Checklist
## Backend Capabilities
- `get_storage_path / get_storage_dir / get_state_db_path`
- `read_profile / write_profile`
- `generate_profile`
- `sync_service_machine_id / sync_service_machine_id_from_storage`
- `load_global_original / save_global_original`
- `get_device_profiles`
- `bind_device_profile / bind_device_profile_with_profile`
- `restore_device_version / delete_device_version`
- `restore_original_device`
- `open_device_folder`
## UI Capabilities
- Show current storage / account binding / history / baseline
- Generate and bind
- Capture and bind
- Restore original fingerprint
- Restore history revision
- Delete history revision
- Open storage folder
## Switching Behavior
- Local account switching applies bound fingerprint
- Cloud account switching applies bound fingerprint
- Concurrent switch requests use queued serial semantics
- Failure semantics are fail-fast (no extra transaction orchestration)
## Compatibility Scope
- Legacy data missing optional fields: fill defaults
- Corrupted structure/type mismatch: explicit failure
## Status Marking
Please mark each item as: `Passed / Failed / Diverged (with reason)`.
@@ -0,0 +1,50 @@
## Context
Device-fingerprint capability spans the data layer, switch-orchestration layer, and UI management layer.
Historical proposals split the same capability across multiple change IDs, which fragmented implementation and acceptance criteria.
This design combines "feature completeness + robustness" into a single constraint set to keep implementation, verification, and release aligned.
## Goals / Non-Goals
- Goals:
- Unify scope and acceptance criteria for device-fingerprint capability.
- Ensure behavioral consistency and diagnosability for local/cloud switching.
- Keep consistent behavior on default paths across Windows/macOS/Linux.
- Keep legacy-data compatibility predictable and verifiable.
- Non-Goals:
- Support for non-default runtime configuration paths.
- Business strategy expansion unrelated to this capability.
## Decisions
- Decision: Use a single change ID to manage the complete capability.
- Rationale: Reduce governance overhead caused by duplicate proposals.
- Decision: Apply fingerprint using a two-phase flow with strict pre-commit verification.
- Rationale: Reduce mixed-state risk caused by partial success.
- Decision: Use versioned payload + boundary validation for fingerprint/history data.
- Rationale: Avoid silent data contamination and define clear compatibility/failure boundaries.
- Decision: Use fail-fast semantics for switch failures and return structured failure reasons.
- Rationale: Make failure behavior predictable and observable.
- Decision: Trigger safe mode for temporary degradation after consecutive failures.
- Rationale: Keep switching flow available during fault windows.
## Risks / Trade-offs
- Risk: Stricter validation may reject legacy dirty data.
- Mitigation: Normalize missing optional fields; fail-fast only for critical structure errors.
- Risk: Validation and diagnostic extensions add minor performance cost.
- Mitigation: Keep validation lightweight and limited to critical boundaries.
- Risk: More UI management actions may increase user misoperations.
- Mitigation: Add confirmation interactions and unit-test coverage for failure branches.
## Migration Plan
1. Use `update-device-fingerprint-hardening` as the single active proposal.
2. Merge historical duplicate proposal content into this proposal (`proposal`/`design`/`tasks`/`spec`).
3. Close and remove duplicate proposal directories.
4. Deliver with unified task checklist and acceptance checklist.
## Open Questions
- Should safe mode exit only by time window, or also allow manual recovery?
- Should diagnostic snapshots be exposed in the renderer as a visual panel?
@@ -0,0 +1,39 @@
# Change: Device Fingerprint Consolidation and Hardening
## What Changes
- Unify backend scope:
- Fingerprint read/write with synchronized updates to `storage.json` and `state.vscdb`
- Account-level binding, revision history, and baseline restore
- Fingerprint apply orchestration during switch flows (local/cloud)
- Unify management scope:
- Device fingerprint management dialog
- Generate-and-bind, capture-and-bind, restore revision, delete revision, restore baseline, and open storage folder
- Unify robustness scope:
- Two-phase apply (`prepare -> verify -> commit`)
- Deterministic rollback before commit on failure
- Failure reason classification, safe mode, degradation strategy, and diagnostics snapshots
- Versioned payload with boundary validation (lenient compatibility + strict failure for critical structure)
- Path scope: this stage focuses on the default runtime configuration path.
## Non-Goals
- Do not promise any anti-detection, anti-ban, or risk-control bypass outcome.
- Do not introduce token/protobuf semantic changes unrelated to device fingerprinting.
- Do not perform ORM or large database refactors in this change.
## Impact
- Affected specs: `device-fingerprint-hardening`
- Affected code:
- `src/ipc/device/handler.ts`
- `src/ipc/account/handler.ts`
- `src/ipc/cloud/handler.ts`
- `src/ipc/switchFlow.ts`
- `src/ipc/switchMetrics.ts`
- `src/ipc/database/cloudHandler.ts`
- `src/components/DeviceFingerprintDialog.tsx`
- `src/actions/account.ts`
- `src/actions/cloud.ts`
- `src/tests/unit/account.test.ts`
- `src/tests/unit/cloudHandler-sync.test.ts`
@@ -0,0 +1,109 @@
## ADDED Requirements
### Requirement: Complete Device Fingerprint Management Capability
The system SHALL provide complete device-fingerprint management capability: view current storage fingerprint, account-bound fingerprint, history revisions, and baseline fingerprint, and execute generate/capture/bind/restore/delete operations.
#### Scenario: Open device fingerprint management
- **WHEN** an operator opens device fingerprint management for a specific account
- **THEN** the system returns current storage fingerprint, account-bound fingerprint, history revisions, and baseline fingerprint (if present)
#### Scenario: Generate and bind a new fingerprint
- **WHEN** the operator confirms "Generate and Bind"
- **THEN** the system generates a valid new fingerprint and binds it to the target account
- **AND** the new revision is appended to history and marked as current
#### Scenario: Capture current and bind
- **WHEN** the operator confirms "Capture Current and Bind"
- **THEN** the system reads the current storage fingerprint and binds it to the target account
- **AND** a new revision is appended to history
### Requirement: Switch Flow Must Apply Account-Bound Fingerprint
The system SHALL ensure the target account has a valid bound fingerprint before continuing switch flow for both local-account and cloud-account switching.
#### Scenario: Target account has no bound fingerprint during switching
- **GIVEN** the target account exists and has no bound fingerprint
- **WHEN** account switching is initiated
- **THEN** the system automatically generates and binds a usable fingerprint
- **AND** then continues with the switch flow
#### Scenario: Concurrent switch requests
- **WHEN** multiple switch requests are triggered while a switch is in progress
- **THEN** requests are queued and processed serially
- **AND** no concurrent write conflicts occur
### Requirement: Fingerprint Apply Must Preserve Storage Consistency
The system SHALL update `storage.json` and `state.vscdb` in sync when applying device fingerprint, and SHALL ensure `storage.serviceMachineId` equals `devDeviceId`.
#### Scenario: Normal fingerprint apply
- **WHEN** the system applies a device fingerprint
- **THEN** telemetry fields in `storage.json` are updated
- **AND** `storage.serviceMachineId` is updated to `devDeviceId`
- **AND** `ItemTable['storage.serviceMachineId']` in `state.vscdb` is updated accordingly
### Requirement: Device Fingerprint Apply Uses Two-Phase Protocol
The system SHALL execute device-fingerprint apply with a two-phase protocol (`prepare -> verify -> commit`), and SHALL perform deterministic rollback when failure occurs before commit.
#### Scenario: Failure during verify phase
- **WHEN** prepare writes succeed but verify detects inconsistent state
- **THEN** the system rolls back to pre-apply state
- **AND** returns a classifiable failure reason
#### Scenario: Write failure before commit
- **WHEN** any critical write fails before commit completes
- **THEN** the system performs rollback
- **AND** post-operation state equals pre-operation state
### Requirement: Fingerprint Data Uses Versioned Structure and Boundary Validation
The system SHALL use explicit schema versions for fingerprint and history data, and SHALL validate at read/write boundaries.
#### Scenario: Missing optional fields
- **WHEN** history payload lacks optional fields
- **THEN** the system fills default values and continues
#### Scenario: Critical structure corruption or type mismatch
- **WHEN** payload has critical structure corruption or critical field type mismatch
- **THEN** the system fails fast and returns a readable error
### Requirement: Consecutive Failures Trigger Safe Degradation
The system SHALL enter safe mode when consecutive device-fingerprint apply failures reach a threshold, temporarily disable fingerprint apply, keep switching available, and emit warnings.
#### Scenario: Consecutive-failure threshold reached
- **WHEN** consecutive failures reach the threshold
- **THEN** safe mode is activated and fingerprint apply is temporarily disabled
- **AND** switch flow remains available under warning conditions
### Requirement: Provide Observable Diagnostic Snapshot
The system SHALL expose a read-only diagnostics interface returning switch-guard state, switch counters, failure-reason counters, latest failure metadata, and safe-mode state.
#### Scenario: Query diagnostics after failures
- **WHEN** diagnostics are queried after one or more failures
- **THEN** reason-classified failure counters are returned
- **AND** latest failure details and safe-mode state are returned
### Requirement: Path Resolution Scope Constraint
The system SHALL use default runtime configuration paths for path resolution in this capability.
#### Scenario: Use default runtime configuration paths
- **WHEN** the system performs device-fingerprint-related path resolution
- **THEN** resolution is completed based on default runtime configuration paths
@@ -0,0 +1,44 @@
## 1. Proposal Consolidation
- [x] 1.1 Merge duplicate device-fingerprint proposals into `update-device-fingerprint-hardening`.
- [x] 1.2 Unify scope and terminology across proposal/design/spec.
- [ ] 1.3 Complete proposal archival strategy (archive after release).
## 2. Feature Completeness (Backend)
- [x] 2.1 Provide APIs for querying/binding/restoring/deleting fingerprint history.
- [x] 2.2 Provide API to restore original device fingerprint.
- [x] 2.3 Provide API to open fingerprint storage folder.
- [x] 2.4 Reuse unified fingerprint-apply chain for local and cloud switching.
## 3. Feature Completeness (UI)
- [x] 3.1 Provide management entry via `DeviceFingerprintDialog`.
- [x] 3.2 Support generate-and-bind, capture-and-bind, restore history, and delete history.
- [x] 3.3 Support restore original fingerprint and open storage folder.
- [x] 3.4 Complete i18n copy and error messaging.
## 4. Robustness Hardening
- [x] 4.1 Implement two-phase apply in `src/ipc/device/handler.ts` (`prepare -> verify -> commit`).
- [x] 4.2 Perform deterministic rollback on pre-commit failure.
- [x] 4.3 Introduce failure-reason classification and integrate with `src/ipc/switchMetrics.ts`.
- [x] 4.4 Introduce versioned payload and strict boundary validation.
- [x] 4.5 Introduce safe mode and recovery strategy.
- [x] 4.6 Extend diagnostics snapshot (failure-reason counts, latest failure metadata, safe-mode status).
## 5. Verification
- [x] 5.1 Run `npm run type-check`.
- [x] 5.2 Run targeted unit tests:
- `npm run test:unit src/tests/unit/account.test.ts`
- `npm run test:unit src/tests/unit/cloudHandler-sync.test.ts`
- [ ] 5.3 Add/extend rollback invariant unit tests.
- [ ] 5.4 Add/extend abnormal history-data behavior unit tests.
- [ ] 5.5 Add/extend unit tests for platform-constrained generation rules.
- [ ] 5.6 Complete manual acceptance matrix across Windows/macOS/Linux.
## 6. Delivery Documentation
- [ ] 6.1 Produce a unified acceptance checklist and mark status item by item.
- [ ] 6.2 Archive merged proposals after release.
@@ -0,0 +1,38 @@
## Context
- The target IDE changed account OAuth token persistence format starting from version 1.16.5.
- Existing JS logic only covers the legacy format, causing account detection and switching failures.
- Version detection, new/legacy routing, and dual-write fallback need to be completed.
## Goals / Non-Goals
- Goals:
- Ensure compatibility for versions 1.16.5+.
- Apply dual-format injection fallback when version detection fails.
- Extend backup/restore coverage to include new-format keys.
## Decisions
- Decision: Write a single format when version detection succeeds; dual-write when detection fails.
- Decision: Prefer reading the new format for account detection, with legacy fallback.
- Decision: Keep protobuf encode/decode behavior and field ordering consistent.
## Risks / Trade-offs
- Risk: Protobuf encode/decode mismatch may cause injection failure.
- Mitigation: Per-field validation and regression test coverage.
- Risk: Version detection failure.
- Mitigation: Dual-write fallback.
- Risk: DB lock conflicts.
- Mitigation: Keep existing flow: close IDE -> backup -> inject -> restart.
## Migration Plan
1. Implement version-detection module (JS).
2. Implement protobuf encode/decode for new format.
3. Implement routed injection/detection logic with dual-write fallback.
4. Extend backup/restore key set.
## Open Questions
- None.
@@ -0,0 +1,34 @@
# Change: IDE Auth Compatibility Fix for 1.16.5+
## Why
- The target IDE changed OAuth token persistence format starting from 1.16.5.
- The current JS implementation only supports the legacy format, causing account detection and switching failures.
- The verified behavior needs to be fully migrated into the current code path to reduce regression risk.
## What Changes
- Add IDE version detection and route injection by legacy/new format.
- Support encoding/decoding and account detection for the new OAuth token format.
- Apply dual-write fallback (legacy + new) when version detection fails.
- Extend backup/restore logic to include new-format keys.
## Constraints
- Keep existing behavior semantics; do not introduce new strategy layers or business fields.
- This stage only covers the default runtime configuration path.
## Impact
- Affected specs: `ide-auth-compat`
- Affected code:
- `src/ipc/database/cloudHandler.ts`
- `src/utils/protobuf.ts`
- `src/ipc/database/handler.ts`
- `src/utils/paths.ts`
## Risks
- Protobuf encoding/decoding mismatch may break injection or parsing.
- Version detection failure may route to the wrong injection path.
- DB lock conflicts during injection may cause write failures.
@@ -0,0 +1,35 @@
## ADDED Requirements
### Requirement: New-Format Injection for 1.16.5+
The system SHALL write only the new format `antigravityUnifiedStateSync.oauthToken` when IDE version is >= 1.16.5.
#### Scenario: New-version injection
- **WHEN** IDE version is 1.16.5 or above
- **THEN** the system writes the new format and completes account switching
### Requirement: Legacy Injection for Older Versions
The system SHALL write only the legacy format `jetskiStateSync.agentManagerInitState` when IDE version is < 1.16.5.
#### Scenario: Legacy-version injection
- **WHEN** IDE version is below 1.16.5
- **THEN** the system writes the legacy format and completes account switching
### Requirement: Dual-Write Fallback on Version Detection Failure
The system SHALL attempt both new and legacy format injection when IDE version cannot be detected.
#### Scenario: Version detection fails
- **WHEN** the system cannot read IDE version
- **THEN** the system writes both new and legacy formats and must not abort due to one path failing
### Requirement: Account Detection via New Format
The system SHALL read OAuth token from the new format and complete account detection.
#### Scenario: Only new format exists
- **WHEN** IDE database contains only the new-format key
- **THEN** the system still recognizes accounts and completes synchronization
### Requirement: Backup/Restore Includes New-Format Keys
The system SHALL include new-format keys in account backup and restore.
#### Scenario: Backup and restore for new format
- **WHEN** users perform backup and restore
- **THEN** new-format keys are fully saved and restored
@@ -0,0 +1,19 @@
## 1. Scope and Planning Confirmation
- [x] 1.1 Review `proposal.md`.
- [x] 1.2 Confirm this stage covers default runtime configuration path only.
- [x] 1.3 Define behavior-consistency constraints.
## 2. Implementation
- [x] 2.1 Add IDE version-detection module.
- [x] 2.2 Add encode/decode support for new-format OAuth token.
- [x] 2.3 Add routed injection logic and dual-write fallback on detection failure.
- [x] 2.4 Prefer new format for account detection, with legacy fallback.
- [x] 2.5 Extend backup/restore to cover new-format keys.
## 3. Verification
- [ ] 3.1 Self-test injection paths for both new and legacy formats.
- [ ] 3.2 Verify dual-write behavior when version detection fails.
- [ ] 3.3 Verify account availability after backup/restore.
+99 -96
View File
@@ -2,105 +2,108 @@
## Purpose
Antigravity Manager 是一个综合性的 Antigravity 增强工具,旨在打破 AI 开发的资源限制。它不仅解决了 Antigravity IDE 客户端无法原生支持多账号切换的痛点,还引入了企业级的 AI 服务账号池管理功能。通过接管应用的配置状态和云端 API 资源,它允许用户在无限个本地账号和云端 AI 服务之间无缝切换,实现 "无限" 的 AI 调用体验。
Antigravity Manager is a comprehensive enhancement tool for Antigravity, designed to remove AI development resource limits. It not only solves the lack of native multi-account switching in Antigravity IDE, but also introduces enterprise-grade AI account pool management. By taking control of app configuration state and cloud API resources, it allows users to switch seamlessly between unlimited local accounts and cloud AI services for an effectively "unlimited" AI usage experience.
**核心目标:**
**Core goals:**
* **本地多账号切换**:无缝切换 Antigravity IDE 的本地用户状态,保护数据完整性(自动备份)。
* **AI 资源池管理**(新):集中管理 Google Gemini、Claude 3.5 等 AI 服务的账号池。
* **智能配额监控**(新):实时监控 API 配额(Quota)和健康度,提供可视化仪表盘。
* **自动资源调度**(新):当当前账号配额耗尽时,自动切换到最佳可用账号(Token 保活与轮询)。
* **跨平台支持**:统一的 macOS、Windows、Linux 体验。
* **Local multi-account switching**: Seamlessly switch Antigravity IDE local user state while protecting data integrity (automatic backup).
* **AI resource pool management (new)**: Centrally manage account pools for Google Gemini, Claude 3.5, and similar services.
* **Intelligent quota monitoring (new)**: Monitor API quota and health in real time with a visual dashboard.
* **Automatic resource scheduling (new)**: Automatically switch to the best available account when the current one runs out of quota (token keepalive + polling).
* **Cross-platform support**: Consistent macOS, Windows, and Linux experience.
## Tech Stack
* **Electron 39** - 跨平台桌面应用框架
* **React 19.2** - UI 框架
* **TypeScript 5.9** - 类型安全的开发语言
* **Shadcn UI** - 现代化 UI 组件库
* **Tailwind CSS 4** - 样式框架
* **oRPC** - 类型安全的 IPC 通信
* **TanStack Router** - 文件路由
* **React Query** - 服务器状态管理
* **better-sqlite3** - SQLite 数据库访问
* **Vite** - 构建工具
* **Vitest** - 单元测试
* **Playwright** - 端到端测试
* **Electron Forge** - 应用打包和分发
* **Electron 39** - Cross-platform desktop framework
* **React 19.2** - UI framework
* **TypeScript 5.9** - Type-safe development language
* **Shadcn UI** - Modern UI component library
* **Tailwind CSS 4** - Styling framework
* **oRPC** - Type-safe IPC communication
* **TanStack Router** - File-based routing
* **React Query** - Server state management
* **better-sqlite3** - SQLite data access
* **Vite** - Build tool
* **Vitest** - Unit testing
* **Playwright** - End-to-end testing
* **Electron Forge** - Packaging and distribution
## Project Conventions
### Code Style
* TypeScript strict mode
* ESLint + Prettier 自动格式化
* 函数和变量使用 camelCase
* 组件使用 PascalCase
* 接口使用 PascalCase,以 I 开头(可选)
* 类型使用 PascalCase
* 2 空格缩进
* 单引号字符串
* 尾随逗号
* TypeScript strict mode
* ESLint + Prettier auto-formatting
* Use camelCase for functions and variables
* Use PascalCase for components
* Use PascalCase for interfaces, optionally prefixed with `I`
* Use PascalCase for types
* 2-space indentation
* Single-quoted strings
* Trailing commas
### Architecture Patterns
```
```plaintext
src/
├── main.ts # Electron 主进程
├── preload.ts # 预加载脚本
├── renderer.ts # 渲染进程入口
├── ipc/ # IPC 通信层
│ ├── account/ # 本地 IDE 账号管理 IPC
│ ├── cloud/ # 云端 AI 资源管理 IPC(新)
│ ├── database/ # 数据库操作 IPC
│ └── process/ # 进程管理 IPC
├── services/ # 业务逻辑服务层(新)
│ ├── QuotaService.ts # 配额监控服务
│ └── TokenService.ts # Token 保活服务
├── actions/ # 渲染进程操作(oRPC 客户端)
├── components/ # React 组件
├── routes/ # 页面路由
├── layouts/ # 布局组件
├── utils/ # 工具函数
├── types/ # TypeScript 类型定义
└── tests/ # 测试文件
├── main.ts # Electron main process
├── preload.ts # Preload script
├── renderer.ts # Renderer entry
├── ipc/ # IPC communication layer
│ ├── account/ # Local IDE account IPC
│ ├── cloud/ # Cloud AI resource IPC (new)
│ ├── database/ # Database operation IPC
│ └── process/ # Process management IPC
├── services/ # Business service layer (new)
│ ├── QuotaService.ts # Quota monitoring service
│ └── TokenService.ts # Token keepalive service
├── actions/ # Renderer actions (oRPC client)
├── components/ # React components
├── routes/ # Page routes
├── layouts/ # Layout components
├── utils/ # Utility functions
├── types/ # TypeScript type definitions
└── tests/ # Test files
```
**设计模式:**
**Design patterns:**
* **关注点分离**:主进程处理系统操作和网络代理,渲染进程处理 UI。
* **Service 层**:引入 Service 层处理复杂的配额轮询和 Token 刷新逻辑。
* **类型安全 IPC**:使用 oRPC 确保主进程和渲染进程之间的类型安全通信。
* **状态管理**:React Query 管理服务器状态和异步配额数据。
* **Separation of concerns**: Main process handles system operations and network proxy; renderer handles UI.
* **Service layer**: Encapsulates complex quota polling and token refresh logic.
* **Type-safe IPC**: Uses oRPC to enforce type-safe communication between main and renderer.
* **State management**: React Query manages server state and async quota data.
### Testing Strategy
* **单元测试(Vitest)**:测试 IPC 处理程序、Service 逻辑、工具函数。
* **集成测试(Vitest)**:测试完整的 IPC 流程。
* **端到端测试(Playwright)**:测试用户工作流。
* **覆盖率目标**:核心逻辑 >80%。
* **Unit tests (Vitest)**: IPC handlers, service logic, utility functions.
* **Integration tests (Vitest)**: End-to-end IPC flow.
* **End-to-end tests (Playwright)**: User workflows.
* **Coverage target**: Core logic > 80%.
### Git Workflow
* **主分支**:`main`
* **开发分支**:`develop`
* **提交规范**:语义化提交 (feat, fix, docs, refactor, test, chore)
* **Main branch**: `main`
* **Development branch**: `develop`
* **Commit style**: Conventional commits (`feat`, `fix`, `docs`, `refactor`, `test`, `chore`)
## Domain Context
### 1. Antigravity IDE 数据结构(本地)
### 1. Antigravity IDE Data Structure (Local)
**Database locations:**
**数据库位置:**
* macOS: `~/Library/Application Support/Antigravity/User/globalStorage/state.vscdb`
* Windows: `%APPDATA%/Antigravity/User/globalStorage/state.vscdb`
* Linux: `~/.config/Antigravity/state.vscdb`
* Linux: `~/.config/Antigravity/User/globalStorage/state.vscdb`
**关键数据库键:**
* `antigravityAuthStatus`: 认证状态和用户信息。
**Key database fields:**
### 2. AI 服务资源结构(云端/新)
* `antigravityAuthStatus`: Authentication status and user information.
### 2. AI Service Resource Structure (Cloud / New)
**Account model:**
**账号模型:**
```json
{
"id": "uuid",
@@ -115,49 +118,49 @@ src/
}
```
### 3. 进程管理策略
### 3. Process Management Strategy
* **三阶段关闭**:优雅关闭 -> SIGTERM -> SIGKILL。
* **Three-phase shutdown**: Graceful close -> SIGTERM -> SIGKILL.
## Important Constraints
### 技术约束
### Technical Constraints
* **数据库访问**:访问本地 IDE 数据库时需注意文件锁。
* **网络访问**:需要访问外部 API (Google/Anthropic) 以获取配额信息。
* **凭证安全**:OAuth Token 和 Refresh Token 必须使用系统级加密存储 (Keytar / SQLite Encrypted)。
* **并发控制**:轮询配额时需注意频率,避免触发 API 封控。
* **Database access**: Handle file locks when accessing local IDE databases.
* **Network access**: External API access (Google/Anthropic) is required for quota retrieval.
* **Credential security**: OAuth and refresh tokens must use system-level encrypted storage (Keytar / encrypted SQLite).
* **Concurrency control**: Quota polling frequency must be controlled to avoid API throttling or blocking.
### 用户体验约束
### UX Constraints
* **响应时间**:账号切换应在 5 秒内完成。
* **静默运行**:支持最小化到托盘,后台自动刷新 Token。
* **数据安全**:切换前必须自动备份当前状态。
* **Response time**: Account switching should complete within 5 seconds.
* **Silent mode**: Support tray minimization and background token refresh.
* **Data safety**: Always back up current state before switching.
## External Dependencies
### 系统依赖
### System Dependencies
* **Antigravity IDE**:本地管理目标。
* **SQLite**:本地数据读取。
* **Antigravity IDE**: Local target to manage.
* **SQLite**: Local data reads.
### Node.js 依赖(重点)
### Node.js Dependencies (Key)
* `better-sqlite3`: 数据库访问。
* `electron`: 桌面框架。
* `node-fetch` / `axios`: 网络请求。
* `tar` / `adm-zip`: 备份压缩。
* `better-sqlite3`: Database access.
* `electron`: Desktop framework.
* `node-fetch` / `axios`: Network requests.
* `tar` / `adm-zip`: Backup compression.
### 外部服务
### External Services
* **Google Gemini API**: 用于配额检查。
* **Anthropic API**: 用于配额检查。
* **OAuth 2.0 端点**: 用于账号授权。
* **Google Gemini API**: For quota checks.
* **Anthropic API**: For quota checks.
* **OAuth 2.0 endpoints**: For account authorization.
### 文件系统依赖
### Filesystem Dependencies
* `~/.antigravity-agent/`
* `accounts.json`: 本地 IDE 账号索引。
* `cloud_accounts.db`: (新) 云端账号池数据库 (SQLite)。
* `backups/`: 备份文件。
* `app.log`: 应用日志。
* `~/.antigravity-agent/`
* `accounts.json`: Local IDE account index.
* `cloud_accounts.db`: (new) Cloud account pool database (SQLite).
* `backups/`: Backup files.
* `app.log`: Application log.
+13703 -135
View File
File diff suppressed because it is too large. Load diff
+42 -11
View File
@@ -1,7 +1,7 @@
{
"name": "antigravity-manager",
"productName": "Antigravity Manager",
"version": "0.0.1",
"version": "0.8.0",
"description": "Antigravity Manager - Electron App",
"main": ".vite/build/main.js",
"private": true,
@@ -18,16 +18,17 @@
"test:unit": "vitest",
"test:e2e": "playwright test",
"test:all": "vitest run && playwright test",
"test-db": "electron test-db.js",
"type-check": "tsc --noEmit"
},
"author": "Draculabo",
"license": "MIT",
"license": "CC-BY-NC-SA-4.0",
"devDependencies": {
"@electron-forge/cli": "^7.10.2",
"@electron-forge/maker-deb": "^7.10.2",
"@electron-forge/maker-dmg": "^7.10.2",
"@electron-forge/maker-rpm": "^7.10.2",
"@electron-forge/maker-squirrel": "^7.10.2",
"@electron-forge/maker-wix": "^7.10.2",
"@electron-forge/maker-zip": "^7.10.2",
"@electron-forge/plugin-auto-unpack-natives": "^7.10.2",
"@electron-forge/plugin-fuses": "^7.10.2",
@@ -36,10 +37,15 @@
"@electron-forge/shared-types": "^7.8.1",
"@electron/fuses": "~1.8.0",
"@electron/rebuild": "^3.7.1",
"node-addon-api": "^8.3.1",
"@eslint/compat": "^1.4.1",
"@eslint/js": "^9.39.0",
"@pengx17/electron-forge-maker-appimage": "^1.2.1",
"@playwright/test": "^1.56.1",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@semantic-release/github": "^12.0.2",
"@sentry/vite-plugin": "^4.8.0",
"@swc/core": "^1.15.8",
"@tanstack/router-plugin": "^1.135.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
@@ -47,14 +53,17 @@
"@types/better-sqlite3": "^7.6.12",
"@types/electron-squirrel-startup": "^1.0.2",
"@types/eslint-config-prettier": "^6.11.3",
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.10.1",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^4.7.0",
"babel-plugin-react-compiler": "1.0.0",
"code-inspector-plugin": "^1.3.3",
"electron": "^32.3.3",
"conventional-changelog-conventionalcommits": "^9.1.0",
"electron": "^37.3.1",
"electron-devtools-installer": "^4.0.0",
"electron-packager-languages": "^0.6.0",
"electron-playwright-helpers": "^1.8.2",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
@@ -62,18 +71,31 @@
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"globals": "^16.5.0",
"icon-gen": "^5.0.0",
"jsdom": "^27.1.0",
"node-addon-api": "^8.3.1",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.7.1",
"semantic-release": "^25.0.2",
"semantic-release-config-gitmoji": "^1.5.3",
"sharp": "^0.34.5",
"tailwindcss": "^4.1.16",
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.3",
"unplugin-swc": "^1.5.9",
"vite": "^5.4.21",
"vitest": "^4.0.8"
},
"dependencies": {
"@grpc/grpc-js": "^1.14.3",
"@grpc/proto-loader": "^0.8.0",
"@icons-pack/react-simple-icons": "^13.8.0",
"@nestjs/common": "^11.1.10",
"@nestjs/core": "^11.1.10",
"@nestjs/microservices": "^11.1.11",
"@nestjs/platform-express": "^11.1.11",
"@nestjs/platform-fastify": "^11.1.10",
"@orpc/client": "^1.10.4",
"@orpc/server": "^1.11.3",
"@radix-ui/react-avatar": "^1.1.11",
@@ -90,35 +112,44 @@
"@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-toggle": "^1.1.9",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@sentry/electron": "^7.7.0",
"@sentry/integrations": "^7.114.0",
"@tailwindcss/vite": "^4.1.17",
"@tanstack/react-query": "^5.90.7",
"@tanstack/react-router": "^1.139.3",
"@tanstack/react-router-devtools": "^1.135.0",
"@types/uuid": "^11.0.0",
"axios": "^1.13.2",
"better-sqlite3": "^12.4.6",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"drizzle-orm": "^0.39.1",
"electron-squirrel-startup": "^1.0.1",
"fastify": "^5.6.2",
"find-process": "^2.0.0",
"happy-dom": "^20.0.10",
"i18next": "^25.6.3",
"i18next-browser-languagedetector": "^8.2.0",
"keytar": "^7.9.0",
"lodash-es": "^4.17.22",
"lucide-react": "^0.553.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-i18next": "^16.2.4",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
"tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7",
"undici": "^7.16.0",
"update-electron-app": "^3.1.1",
"uuid": "^13.0.0",
"winston": "^3.19.0",
"winston-daily-rotate-file": "^5.0.0",
"yaml": "^2.8.2",
"zod": "^4.1.12"
},
"ts-node": {
"transpileOnly": true,
"compilerOptions": {
"module": "commonjs"
}
}
}
+6 -6
View File
@@ -1,23 +1,23 @@
import { defineConfig, devices } from "@playwright/test";
import { defineConfig, devices } from '@playwright/test';
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./src/tests/e2e",
testDir: './src/tests/e2e',
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: "html",
reporter: 'html',
use: {
trace: "on-first-retry",
trace: 'on-first-retry',
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});
+259
View File
@@ -0,0 +1,259 @@
/**
* Semantic Release Configuration
*
* This configuration uses the specific release rules requested by the user,
* but adapts the plugin configuration to work with standard Conventional Commits
* instead of Gitmoji (since the project history doesn't use emojis).
*/
const REPO_URL = 'https://github.com/Draculabo/AntigravityManager';
const GITHUB_BASE_URL = 'https://github.com';
const FULL_CHANGELOG_URL = `${REPO_URL}/blob/main/CHANGELOG.md`;
const releaseRules = [
{
release: 'minor',
type: 'feat',
},
{
release: 'patch',
type: 'fix',
},
{
release: 'patch',
type: 'perf',
},
{
release: 'patch',
type: 'style',
},
{
release: 'patch',
type: 'refactor',
},
{
release: 'patch',
type: 'build',
},
{ release: 'patch', scope: 'README', type: 'docs' },
{ release: 'patch', scope: 'README.md', type: 'docs' },
{ release: false, type: 'docs' },
{
release: false,
type: 'test',
},
{
release: false,
type: 'ci',
},
{
release: false,
type: 'chore',
},
{
release: false,
type: 'wip',
},
{
release: 'major',
type: 'BREAKING CHANGE',
},
{
release: 'major',
scope: 'BREAKING CHANGE',
},
{
release: 'major',
subject: '*BREAKING CHANGE*',
},
{ release: 'patch', subject: '*force release*' },
{ release: 'patch', subject: '*force patch*' },
{ release: 'minor', subject: '*force minor*' },
{ release: 'major', subject: '*force major*' },
{ release: false, subject: '*skip release*' },
];
const getGithubUsernameFromEmail = (email) => {
if (!email) {
return undefined;
}
const match = email.match(/^(?:\d+\+)?(?<username>[a-z0-9-]+)@users\.noreply\.github\.com$/i);
return match?.groups?.username;
};
const isBotIdentity = (value) => {
return typeof value === 'string' && /bot/i.test(value);
};
const buildReleaseFooter = (commits) => {
const mergedPullRequests = new Map();
const contributors = new Map();
(commits || []).forEach((commit) => {
const message = [commit?.message, commit?.subject, commit?.header].filter(Boolean).join('\n');
if (message) {
const mergeMatches = message.match(/pull request #(?<number>\d+)/gi);
if (mergeMatches) {
mergeMatches.forEach((match) => {
const number = match.replace(/\D/g, '');
if (number) {
mergedPullRequests.set(number, {
number,
url: `${REPO_URL}/pull/${number}`,
});
}
});
}
const squashMatches = message.match(/\(#(?<number>\d+)\)/g);
if (squashMatches) {
squashMatches.forEach((match) => {
const number = match.replace(/\D/g, '');
if (number) {
mergedPullRequests.set(number, {
number,
url: `${REPO_URL}/pull/${number}`,
});
}
});
}
}
const author = commit?.author || commit?.committer;
const username = getGithubUsernameFromEmail(author?.email);
const displayName = username ? `@${username}` : author?.name;
if (!displayName || isBotIdentity(displayName)) {
return;
}
if (username && isBotIdentity(username)) {
return;
}
contributors.set(username || displayName, {
name: displayName,
url: username ? `${GITHUB_BASE_URL}/${username}` : undefined,
});
});
const footerLines = [];
const sortedPullRequests = Array.from(mergedPullRequests.values()).sort(
(left, right) => Number(left.number) - Number(right.number),
);
if (sortedPullRequests.length > 0) {
footerLines.push('### 🔀 Merged Pull Requests');
sortedPullRequests.forEach((pullRequest) => {
footerLines.push(`- [#${pullRequest.number}](${pullRequest.url})`);
});
footerLines.push('');
}
const sortedContributors = Array.from(contributors.values()).sort((left, right) =>
left.name.localeCompare(right.name),
);
if (sortedContributors.length > 0) {
footerLines.push('### 🙌 Contributors');
sortedContributors.forEach((contributor) => {
if (contributor.url) {
footerLines.push(`- [${contributor.name}](${contributor.url})`);
return;
}
footerLines.push(`- ${contributor.name}`);
});
footerLines.push('');
}
footerLines.push(`Full Changelog: ${FULL_CHANGELOG_URL}`);
return footerLines.join('\n');
};
const appendReleaseNotes = (notes, commits) => {
const trimmedNotes = (notes || '').trimEnd();
const footer = buildReleaseFooter(commits);
if (!footer) {
return trimmedNotes;
}
return `${trimmedNotes}\n\n${footer}`;
};
const appendReleaseNotesPlugin = {
publish: async (pluginConfig, context) => {
if (!context?.nextRelease?.notes) {
return;
}
context.nextRelease.notes = appendReleaseNotes(context.nextRelease.notes, context.commits);
},
};
const releaseConfig = {
branches: ['main', { name: 'beta', prerelease: true }],
plugins: [
[
'@semantic-release/commit-analyzer',
{
preset: 'conventionalcommits',
releaseRules: releaseRules,
},
],
[
'@semantic-release/release-notes-generator',
{
preset: 'conventionalcommits',
presetConfig: {
types: [
{ type: 'feat', section: '✨ Features' },
{ type: 'fix', section: '🐛 Bug Fixes' },
{ type: 'perf', section: '⚡ Performance Improvements' },
{ type: 'revert', section: '⏪ Reverts' },
{ type: 'docs', section: '📝 Documentation' },
{ type: 'style', section: '💄 Styles' },
{ type: 'refactor', section: '♻️ Code Refactoring' },
{ type: 'test', section: '✅ Tests' },
{ type: 'build', section: '👷 Build System' },
{ type: 'ci', section: '🔧 Continuous Integration' },
],
},
},
],
[
'@semantic-release/changelog',
{
changelogFile: 'CHANGELOG.md',
changelogTitle: '<a name="readme-top"></a>\n\n# Changelog',
},
],
'@semantic-release/npm', // Updates package.json and npm-shrinkwrap.json
appendReleaseNotesPlugin,
[
'@semantic-release/github',
{
successComment: false,
failComment: false,
labels: false,
releaseName: 'v${nextRelease.version}',
},
],
[
'@semantic-release/git',
{
assets: ['CHANGELOG.md', 'package.json', 'package-lock.json', 'npm-shrinkwrap.json'],
message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}',
},
],
],
};
releaseConfig.__internal = {
appendReleaseNotes,
buildReleaseFooter,
};
module.exports = releaseConfig;
+149
View File
@@ -0,0 +1,149 @@
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { Rocket } from 'lucide-react';
import sharp from 'sharp';
import icongen from 'icon-gen';
import path from 'path';
import fs from 'fs';
const IMAGES_DIR = path.join(process.cwd(), 'images');
// Colors
const BACKGROUND_COLOR = '#0f172a'; // slate-900 / primary
const ICON_COLOR = '#f8fafc'; // slate-50 / primary-foreground
const ICON_SIZE = 1024;
const PADDING = 0.25; // 25% padding
async function generate() {
console.log('Generating assets...');
// 1. Generate SVG String
// We want the icon to be centered and sized appropriately.
// The canvas is 1024x1024.
// We will create an SVG with the background rect and the icon centered.
// Lucide icons are 24x24 by default.
// We will scale it up.
const iconSize = ICON_SIZE * (1 - PADDING * 2);
const padding = ICON_SIZE * PADDING;
const iconSvg = renderToStaticMarkup(
React.createElement(Rocket, {
size: iconSize,
color: ICON_COLOR,
strokeWidth: 1.5, // Thicker stroke for larger icon? Or keep 2? 1.5 might look elegant.
}),
);
// Construct the full SVG
// We place the icon at (padding, padding)
const fullSvg = `
<svg width="${ICON_SIZE}" height="${ICON_SIZE}" viewBox="0 0 ${ICON_SIZE} ${ICON_SIZE}" xmlns="http://www.w3.org/2000/svg">
<rect width="${ICON_SIZE}" height="${ICON_SIZE}" fill="${BACKGROUND_COLOR}" />
<g transform="translate(${padding}, ${padding})">
${iconSvg}
</g>
</svg>
`;
// 2. Generate Base PNG
const pngBuffer = await sharp(Buffer.from(fullSvg)).png().toBuffer();
const iconPngPath = path.join(IMAGES_DIR, 'icon.png');
await sharp(pngBuffer).toFile(iconPngPath);
console.log(`Generated ${iconPngPath}`);
// 3. Generate ICO and ICNS using icon-gen
// icon-gen takes the png directory or file and outputs to destination
// Create a temporary directory for input if needed, but icon-gen works with a file path usually?
// Checking icon-gen docs or usage. commonly: icongen(input, destination, options)
// Input can be a path to a png image.
// Clean up old directories and files
const itemsToClean = [
'android',
'ios',
'icon.iconset',
'icon_master_squircle.png',
'storealogo.png',
'tray-icon.png', // incorrectly named
// Clean up "Square" logos as they appear unused
'Square30x30Logo.png',
'Square44x44Logo.png',
'Square71x71Logo.png',
'Square89x89Logo.png',
'Square107x107Logo.png',
'Square142x142Logo.png',
'Square150x150Logo.png',
'Square284x284Logo.png',
'Square310x310Logo.png',
'StoreLogo.png',
]; // checking casing or existence
for (const item of itemsToClean) {
const itemPath = path.join(IMAGES_DIR, item);
if (fs.existsSync(itemPath)) {
fs.rmSync(itemPath, { recursive: true, force: true });
console.log(`Removed old item: ${itemPath}`);
}
}
const options = {
report: true,
ico: {
name: 'icon',
sizes: [16, 24, 32, 48, 64, 128, 256],
},
icns: {
name: 'icon',
sizes: [16, 32, 64, 128, 256, 512, 1024],
},
favicon: {
name: 'favicon-',
pngSizes: [32, 57, 72, 96, 120, 128, 144, 152, 195, 228],
icoSizes: [16, 24, 32, 48, 64],
},
};
await icongen(iconPngPath, IMAGES_DIR, options);
console.log('Generated ICO, ICNS, and Favicon files');
// Generate standard PNGs only
const standardSizes = [
{ name: '32x32.png', size: 32 },
{ name: '64x64.png', size: 64 },
{ name: '128x128.png', size: 128 },
{ name: '[email protected]', size: 256 },
];
for (const { name, size } of standardSizes) {
await sharp(pngBuffer).resize(size, size).toFile(path.join(IMAGES_DIR, name));
console.log(`Generated ${name}`);
}
// Generate tray.png (Matched to code requirement)
const TRAY_SIZE = 24;
// Code uses 'tray.png'
const trayIconSvg = renderToStaticMarkup(
React.createElement(Rocket, {
size: TRAY_SIZE,
color: ICON_COLOR, // White
strokeWidth: 2,
}),
);
const trayPngPath = path.join(IMAGES_DIR, 'tray.png');
await sharp(Buffer.from(trayIconSvg)).png().toFile(trayPngPath);
console.log('Generated tray.png');
// Sync to src/assets
const SRC_ASSETS_DIR = path.join(process.cwd(), 'src', 'assets');
if (fs.existsSync(SRC_ASSETS_DIR)) {
fs.copyFileSync(iconPngPath, path.join(SRC_ASSETS_DIR, 'icon.png'));
fs.copyFileSync(trayPngPath, path.join(SRC_ASSETS_DIR, 'tray.png'));
console.log('Synced icon.png and tray.png to src/assets/');
}
}
generate().catch(console.error);
+2
View File
@@ -8,6 +8,7 @@ import { router } from './utils/routes';
import './localization/i18n';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider } from '@/components/theme-provider';
import { Toaster } from '@/components/ui/toaster';
import { LOCAL_STORAGE_KEYS } from '@/constants';
function App() {
@@ -32,6 +33,7 @@ root.render(
<QueryClientProvider client={queryClient}>
<ThemeProvider storageKey={LOCAL_STORAGE_KEYS.THEME} defaultTheme="system">
<App />
<Toaster />
</ThemeProvider>
</QueryClientProvider>
</React.StrictMode>,
+38 -1
View File
@@ -1,4 +1,5 @@
import { ipc } from "@/ipc/manager";
import { ipc } from '@/ipc/manager';
import type { DeviceProfile } from '@/types/account';
export function listAccounts() {
return ipc.client.account.listAccounts();
@@ -15,3 +16,39 @@ export function switchAccount(accountId: string) {
export function deleteAccount(accountId: string) {
return ipc.client.account.deleteAccount({ accountId });
}
export function previewGenerateIdentityProfile() {
return ipc.client.account.previewGenerateIdentityProfile();
}
export function getIdentityProfiles(accountId: string) {
return ipc.client.account.getIdentityProfiles({ accountId });
}
export function bindIdentityProfile(accountId: string, mode: 'capture' | 'generate') {
return ipc.client.account.bindIdentityProfile({ accountId, mode });
}
export function bindIdentityProfileWithPayload(accountId: string, profile: DeviceProfile) {
return ipc.client.account.bindIdentityProfileWithPayload({ accountId, profile });
}
export function applyBoundIdentityProfile(accountId: string) {
return ipc.client.account.applyBoundIdentityProfile({ accountId });
}
export function restoreIdentityProfileRevision(accountId: string, versionId: string) {
return ipc.client.account.restoreIdentityProfileRevision({ accountId, versionId });
}
export function deleteIdentityProfileRevision(accountId: string, versionId: string) {
return ipc.client.account.deleteIdentityProfileRevision({ accountId, versionId });
}
export function restoreBaselineProfile(accountId: string) {
return ipc.client.account.restoreBaselineProfile({ accountId });
}
export function openIdentityStorageFolder() {
return ipc.client.account.openIdentityStorageFolder();
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { ipc } from "@/ipc/manager";
import { ipc } from '@/ipc/manager';
export function getPlatform() {
return ipc.client.app.currentPlatfom();
+40
View File
@@ -1,4 +1,5 @@
import { ipc } from '@/ipc/manager';
import type { DeviceProfile } from '@/types/account';
export function addGoogleAccount(input: { authCode: string }) {
return ipc.client.cloud.addGoogleAccount(input);
@@ -39,3 +40,42 @@ export function syncLocalAccount() {
export function startAuthFlow() {
return ipc.client.cloud.startAuthFlow();
}
export function getSwitchStatus() {
return ipc.client.cloud.getSwitchStatus();
}
export function getCloudIdentityProfiles(input: { accountId: string }) {
return ipc.client.cloud.getIdentityProfiles(input);
}
export function previewGenerateCloudIdentityProfile() {
return ipc.client.cloud.previewIdentityProfile();
}
export function bindCloudIdentityProfile(input: { accountId: string; mode: 'capture' | 'generate' }) {
return ipc.client.cloud.bindIdentityProfile(input);
}
export function bindCloudIdentityProfileWithPayload(input: {
accountId: string;
profile: DeviceProfile;
}) {
return ipc.client.cloud.bindIdentityProfileWithPayload(input);
}
export function restoreCloudIdentityProfileRevision(input: { accountId: string; versionId: string }) {
return ipc.client.cloud.restoreIdentityProfileRevision(input);
}
export function restoreCloudBaselineProfile(input: { accountId: string }) {
return ipc.client.cloud.restoreBaselineProfile(input);
}
export function deleteCloudIdentityProfileRevision(input: { accountId: string; versionId: string }) {
return ipc.client.cloud.deleteIdentityProfileRevision(input);
}
export function openCloudIdentityStorageFolder() {
return ipc.client.cloud.openIdentityStorageFolder();
}
+2 -2
View File
@@ -1,5 +1,5 @@
import { ipc } from "@/ipc/manager";
import { AccountBackupData, Account } from "@/types/account";
import { ipc } from '@/ipc/manager';
import { AccountBackupData, Account } from '@/types/account';
export function backupAccount(account: Account) {
return ipc.client.database.backupAccount(account);
+5
View File
@@ -0,0 +1,5 @@
import { ipc } from '@/ipc/manager';
export function openLogDirectory() {
return ipc.client.system.openLogDirectory();
}
+9 -11
View File
@@ -1,6 +1,6 @@
import { ThemeMode } from "@/types/theme-mode";
import { ipc } from "@/ipc/manager";
import { LOCAL_STORAGE_KEYS } from "@/constants";
import { ThemeMode } from '@/types/theme-mode';
import { ipc } from '@/ipc/manager';
import { LOCAL_STORAGE_KEYS } from '@/constants';
export interface ThemePreferences {
system: ThemeMode;
@@ -9,9 +9,7 @@ export interface ThemePreferences {
export async function getCurrentTheme(): Promise<ThemePreferences> {
const currentTheme = await ipc.client.theme.getCurrentThemeMode();
const localTheme = localStorage.getItem(
LOCAL_STORAGE_KEYS.THEME,
) as ThemeMode | null;
const localTheme = localStorage.getItem(LOCAL_STORAGE_KEYS.THEME) as ThemeMode | null;
return {
system: currentTheme,
@@ -20,7 +18,7 @@ export async function getCurrentTheme(): Promise<ThemePreferences> {
}
export async function setTheme(newTheme: ThemeMode) {
const isDarkMode = newTheme === "dark";
const isDarkMode = newTheme === 'dark';
await ipc.client.theme.setThemeMode(newTheme);
localStorage.setItem(LOCAL_STORAGE_KEYS.THEME, newTheme);
updateDocumentTheme(isDarkMode);
@@ -28,7 +26,7 @@ export async function setTheme(newTheme: ThemeMode) {
export async function toggleTheme() {
const isDarkMode = await ipc.client.theme.toggleThemeMode();
const newTheme = isDarkMode ? "dark" : "light";
const newTheme = isDarkMode ? 'dark' : 'light';
updateDocumentTheme(isDarkMode);
localStorage.setItem(LOCAL_STORAGE_KEYS.THEME, newTheme);
@@ -37,7 +35,7 @@ export async function toggleTheme() {
export async function syncWithLocalTheme() {
const { local } = await getCurrentTheme();
if (!local) {
setTheme("system");
setTheme('system');
return;
}
@@ -46,8 +44,8 @@ export async function syncWithLocalTheme() {
function updateDocumentTheme(isDarkMode: boolean) {
if (isDarkMode) {
document.documentElement.classList.add("dark");
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove("dark");
document.documentElement.classList.remove('dark');
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { ipc } from "../ipc/manager";
import { ipc } from '../ipc/manager';
export async function minimizeWindow() {
await ipc.client.window.minimizeWindow();
Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 KiB

After

Width:  |  Height:  |  Size: 659 B

+3 -1
View File
@@ -12,6 +12,7 @@ import {
} from '@/components/ui/dropdown-menu';
import { MoreVertical, Trash2, RefreshCw } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import { enUS, zhCN, ru } from 'date-fns/locale';
import { cn } from '@/lib/utils';
import { useTranslation } from 'react-i18next';
@@ -32,7 +33,7 @@ export const AccountCard: React.FC<AccountCardProps> = ({
isSwitching,
isDeleting,
}) => {
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const cardRef = useRef<HTMLDivElement>(null);
const [transform, setTransform] = useState('');
const [glarePosition, setGlarePosition] = useState({ x: 50, y: 50 });
@@ -136,6 +137,7 @@ export const AccountCard: React.FC<AccountCardProps> = ({
{t('account.lastUsed', {
time: formatDistanceToNow(new Date(account.last_used), {
addSuffix: true,
locale: { en: enUS, 'zh-CN': zhCN, ru: ru }[i18n.language] || enUS,
}),
})}
</p>
+108 -16
View File
@@ -18,8 +18,8 @@ import {
} from '@/components/ui/dropdown-menu';
import { Checkbox } from '@/components/ui/checkbox';
import { MoreVertical, Trash, RefreshCw, Box, Power } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import { MoreVertical, Trash, RefreshCw, Box, Power, Fingerprint } from 'lucide-react';
import { formatDistanceToNow, differenceInMinutes, differenceInHours, isBefore } from 'date-fns';
import { Badge } from '@/components/ui/badge';
import { useTranslation } from 'react-i18next';
@@ -28,6 +28,7 @@ interface CloudAccountCardProps {
onRefresh: (id: string) => void;
onDelete: (id: string) => void;
onSwitch: (id: string) => void;
onManageIdentity: (id: string) => void;
isSelected?: boolean;
onToggleSelection?: (id: string, selected: boolean) => void;
isRefreshing?: boolean;
@@ -40,6 +41,7 @@ export function CloudAccountCard({
onRefresh,
onDelete,
onSwitch,
onManageIdentity,
isSelected = false,
onToggleSelection,
isRefreshing,
@@ -50,22 +52,85 @@ export function CloudAccountCard({
// Helpers to get quota color
const getQuotaColor = (percentage: number) => {
if (percentage > 80) return 'text-green-500';
if (percentage > 20) return 'text-yellow-500';
if (percentage > 80) {
return 'text-green-500';
}
if (percentage > 20) {
return 'text-yellow-500';
}
return 'text-red-500';
};
const getQuotaBarColor = (percentage: number) => {
if (percentage > 80) {
return 'bg-emerald-500';
}
if (percentage > 20) {
return 'bg-amber-500';
}
return 'bg-rose-500';
};
const formatTimeRemaining = (dateStr: string) => {
const targetDate = new Date(dateStr);
if (Number.isNaN(targetDate.getTime())) {
return null;
}
const now = new Date();
if (isBefore(targetDate, now)) {
return '0h 0m';
}
// Calculate difference in hours and minutes
const diffHrs = differenceInHours(targetDate, now);
const diffMins = differenceInMinutes(targetDate, now) - diffHrs * 60;
if (diffHrs >= 24) {
const diffDays = Math.floor(diffHrs / 24);
const remainingHrs = diffHrs % 24;
return `${diffDays}d ${remainingHrs}h`;
}
return `${diffHrs}h ${diffMins}m`;
};
const getResetTimeLabel = (resetTime?: string) => {
if (!resetTime) {
return t('cloud.card.resetUnknown');
}
const remaining = formatTimeRemaining(resetTime);
if (!remaining) {
return t('cloud.card.resetUnknown');
}
return `${t('cloud.card.resetPrefix')}: ${remaining}`;
};
const getResetTimeTitle = (resetTime?: string) => {
if (!resetTime) {
return undefined;
}
const resetDate = new Date(resetTime);
if (Number.isNaN(resetDate.getTime())) {
return undefined;
}
return `${t('cloud.card.resetTime')}: ${resetDate.toLocaleString()}`;
};
const modelQuotas = Object.entries(account.quota?.models || {});
return (
<Card
className={`flex flex-col overflow-hidden transition-all ${isSelected ? 'ring-primary border-primary/50 ring-2' : 'hover:border-primary/50'}`}
className={`group flex h-full flex-col overflow-hidden border bg-card transition-all duration-200 hover:border-primary/40 hover:shadow-sm ${isSelected ? 'ring-primary border-primary/50 ring-2' : ''}`}
>
<CardHeader className="group relative flex flex-row items-center gap-4 space-y-0 pb-2">
<CardHeader className="relative flex flex-row items-center gap-4 space-y-0 pb-2">
{/* Selection Checkbox - Visible on hover or selected */}
{onToggleSelection && (
<div
className={`absolute top-2 left-2 z-10 ${isSelected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'} from-background rounded-full bg-gradient-to-br to-transparent p-2 transition-opacity`}
className={`absolute top-2 left-2 z-10 ${isSelected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'} rounded-full bg-background/90 p-2 transition-opacity`}
>
<Checkbox
checked={isSelected}
@@ -82,7 +147,7 @@ export function CloudAccountCard({
className="bg-muted h-10 w-10 rounded-full border"
/>
) : (
<div className="bg-primary/10 text-primary flex h-10 w-10 items-center justify-center rounded-full">
<div className="bg-primary/10 text-primary flex h-10 w-10 items-center justify-center rounded-full border">
{account.name?.[0]?.toUpperCase() || 'A'}
</div>
)}
@@ -94,7 +159,7 @@ export function CloudAccountCard({
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<Button variant="ghost" size="icon" className="h-8 w-8 cursor-pointer rounded-full">
<MoreVertical className="h-4 w-4" />
<span className="sr-only">Menu</span>
</Button>
@@ -111,6 +176,11 @@ export function CloudAccountCard({
{t('cloud.card.refresh')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => onManageIdentity(account.id)}>
<Fingerprint className="mr-2 h-4 w-4" />
{t('cloud.card.identityProfile')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => onDelete(account.id)}
className="text-destructive focus:text-destructive"
@@ -155,6 +225,7 @@ export function CloudAccountCard({
size="sm"
onClick={() => onSwitch(account.id)}
disabled={isSwitching}
className="cursor-pointer"
>
{isSwitching ? (
<RefreshCw className="h-3 w-3 animate-spin" />
@@ -169,15 +240,36 @@ export function CloudAccountCard({
<div className="space-y-2">
{modelQuotas.length > 0 ? (
modelQuotas.map(([modelName, info]) => (
<div key={modelName} className="flex items-center justify-between text-sm">
<span className="text-muted-foreground max-w-[120px] truncate" title={modelName}>
<div
key={modelName}
className="hover:bg-muted/60 grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3 rounded-lg border border-transparent px-2 py-2 text-sm transition-colors hover:border-border/60"
>
<span className="text-muted-foreground min-w-0 truncate" title={modelName}>
{modelName.replace('models/', '')}
</span>
<div className="flex items-center gap-2">
<span className={`font-mono font-bold ${getQuotaColor(info.percentage)}`}>
{info.percentage}%
<div className="flex flex-col items-end gap-1">
<span
className="text-muted-foreground text-[10px] leading-none"
title={getResetTimeTitle(info.resetTime)}
>
{getResetTimeLabel(info.resetTime)}
</span>
<span className="text-muted-foreground text-xs">{t('cloud.card.left')}</span>
<div className="flex items-baseline gap-1.5">
<span
className={`font-mono leading-none font-bold ${getQuotaColor(info.percentage)}`}
>
{info.percentage}%
</span>
<span className="text-muted-foreground text-[10px]">
{t('cloud.card.left')}
</span>
</div>
<div className="bg-muted h-1.5 w-24 overflow-hidden rounded-full">
<div
className={`h-full rounded-full transition-all duration-300 ${getQuotaBarColor(info.percentage)}`}
style={{ width: `${Math.max(0, Math.min(100, info.percentage))}%` }}
/>
</div>
</div>
</div>
))
@@ -190,7 +282,7 @@ export function CloudAccountCard({
</div>
</CardContent>
<CardFooter className="bg-muted/50 text-muted-foreground justify-between p-2 px-4 text-xs">
<CardFooter className="bg-muted/20 text-muted-foreground justify-between border-t p-2 px-4 text-xs">
<span>
{t('cloud.card.used')}{' '}
{formatDistanceToNow(account.last_used * 1000, { addSuffix: true })}
+197 -134
View File
@@ -11,6 +11,7 @@ import {
startAuthFlow,
} from '@/hooks/useCloudAccounts';
import { CloudAccountCard } from '@/components/CloudAccountCard';
import { IdentityProfileDialog } from '@/components/IdentityProfileDialog';
import { CloudAccount } from '@/types/cloudAccount';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
@@ -41,12 +42,13 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { getLocalizedErrorMessage } from '@/utils/errorMessages';
// ... (existing code: imports and comments)
export function CloudAccountList() {
const { t } = useTranslation();
const { data: accounts, isLoading, isError } = useCloudAccounts();
const { data: accounts, isLoading, isError, error } = useCloudAccounts();
const refreshMutation = useRefreshQuota();
const deleteMutation = useDeleteCloudAccount();
const addMutation = useAddGoogleAccount();
@@ -61,20 +63,55 @@ export function CloudAccountList() {
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
const [authCode, setAuthCode] = useState('');
const [identityAccount, setIdentityAccount] = useState<CloudAccount | null>(null);
const totalAccounts = accounts?.length || 0;
const activeAccounts = accounts?.filter((account) => account.is_active).length || 0;
const rateLimitedAccounts =
accounts?.filter((account) => account.status === 'rate_limited').length || 0;
const handleAddAccount = (codeVal?: string) => {
const codeToUse = codeVal || authCode;
if (!codeToUse) {
return;
}
addMutation.mutate(
{ authCode: codeToUse },
{
onSuccess: () => {
setIsAddDialogOpen(false);
setAuthCode('');
toast({ title: t('cloud.toast.addSuccess') });
},
onError: (err) => {
toast({
title: t('cloud.toast.addFailed.title'),
description: getLocalizedErrorMessage(err, t),
variant: 'destructive',
});
},
},
);
};
// Listen for Google Auth Code
useEffect(() => {
if (window.electron?.onGoogleAuthCode) {
console.log('[OAuth] Setting up auth code listener, dialog open:', isAddDialogOpen);
const cleanup = window.electron.onGoogleAuthCode((code) => {
console.log('[OAuth] Received auth code via IPC:', code?.substring(0, 10) + '...');
setAuthCode(code);
// Auto submit if dialog is open
if (isAddDialogOpen) {
handleAddAccount(code);
}
// Note: Auto-submit will be triggered by the authCode change effect below
});
return cleanup;
}
}, [isAddDialogOpen]);
}, []);
// Auto-submit when authCode is set and dialog is open
useEffect(() => {
if (authCode && isAddDialogOpen && !addMutation.isPending) {
console.log('[OAuth] Auto-submitting auth code');
handleAddAccount(authCode);
}
}, [authCode, isAddDialogOpen]);
// Batch Operations State
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
@@ -103,7 +140,7 @@ export function CloudAccountList() {
onError: (err) =>
toast({
title: t('cloud.toast.switchFailed'),
description: err.message,
description: getLocalizedErrorMessage(err, t),
variant: 'destructive',
}),
},
@@ -130,6 +167,11 @@ export function CloudAccountList() {
}
};
const handleManageIdentity = (id: string) => {
const target = (accounts || []).find((item) => item.id === id) || null;
setIdentityAccount(target);
};
const handleToggleAutoSwitch = (checked: boolean) => {
setAutoSwitchMutation.mutate(
{ enabled: checked },
@@ -145,8 +187,15 @@ export function CloudAccountList() {
};
const handleForcePoll = () => {
if (forcePollMutation.isPending) return;
forcePollMutation.mutate(undefined, {
onSuccess: () => toast({ title: t('cloud.polling') }),
onError: (err) =>
toast({
title: t('cloud.toast.pollFailed'),
description: getLocalizedErrorMessage(err, t),
variant: 'destructive',
}),
});
};
@@ -169,35 +218,13 @@ export function CloudAccountList() {
onError: (err) => {
toast({
title: t('cloud.toast.syncFailed.title'),
description: err.message,
description: getLocalizedErrorMessage(err, t),
variant: 'destructive',
});
},
});
};
const handleAddAccount = (codeVal?: string) => {
const codeToUse = codeVal || authCode;
if (!codeToUse) return;
addMutation.mutate(
{ authCode: codeToUse },
{
onSuccess: () => {
setIsAddDialogOpen(false);
setAuthCode('');
toast({ title: t('cloud.toast.addSuccess') });
},
onError: (err) => {
toast({
title: t('cloud.toast.addFailed.title'),
description: err.message,
variant: 'destructive',
});
},
},
);
};
const openAuthUrl = async () => {
try {
await startAuthFlow();
@@ -264,115 +291,139 @@ export function CloudAccountList() {
}
if (isError) {
return <div className="p-4 text-red-500">Failed to load cloud accounts.</div>;
const errorMessage = getLocalizedErrorMessage(error, t);
return (
<div className="p-4 text-red-500">
<div>{t('cloud.error.loadFailed')}</div>
<div className="mt-1 text-sm text-red-400">{errorMessage}</div>
</div>
);
}
return (
<div className="relative space-y-6 pb-20">
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex shrink-0 flex-col gap-1">
<h2 className="text-2xl font-bold tracking-tight">{t('cloud.title')}</h2>
<p className="text-muted-foreground">{t('cloud.description')}</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<div className="bg-muted/20 flex items-center gap-2 rounded-md border p-2">
<div className="flex items-center gap-2">
<Zap
className={`h-4 w-4 ${autoSwitchEnabled ? 'fill-yellow-500 text-yellow-500' : 'text-muted-foreground'}`}
/>
<Label htmlFor="auto-switch" className="cursor-pointer text-sm font-medium">
{t('cloud.autoSwitch')}
</Label>
<div className="space-y-5 pb-20">
<div className="rounded-lg border bg-card p-5">
<div className="flex flex-wrap items-start justify-between gap-4">
<div className="flex shrink-0 flex-col gap-1">
<h2 className="text-2xl font-bold tracking-tight">{t('cloud.title')}</h2>
<p className="text-muted-foreground max-w-2xl">{t('cloud.description')}</p>
</div>
<div className="flex flex-wrap gap-2">
<div className="bg-muted/50 rounded-md border px-3 py-2">
<div className="text-muted-foreground text-[11px] uppercase">{t('cloud.card.actions')}</div>
<div className="text-base font-semibold">{totalAccounts}</div>
</div>
<Switch
id="auto-switch"
checked={!!autoSwitchEnabled}
onCheckedChange={handleToggleAutoSwitch}
disabled={isSettingsLoading || setAutoSwitchMutation.isPending}
/>
<div className="bg-muted/50 rounded-md border px-3 py-2">
<div className="text-muted-foreground text-[11px] uppercase">{t('cloud.card.active')}</div>
<div className="text-base font-semibold text-emerald-600">{activeAccounts}</div>
</div>
<div className="bg-muted/50 rounded-md border px-3 py-2">
<div className="text-muted-foreground text-[11px] uppercase">{t('cloud.card.rateLimited')}</div>
<div className="text-base font-semibold text-rose-600">{rateLimitedAccounts}</div>
</div>
</div>
<Button variant="ghost" onClick={toggleSelectAll} title={t('cloud.batch.selectAll')}>
<CheckSquare
className={`mr-2 h-4 w-4 ${selectedIds.size > 0 && selectedIds.size === accounts?.length ? 'text-primary fill-primary/20' : ''}`}
/>
{t('cloud.batch.selectAll')}
</Button>
<Button
variant="outline"
size="icon"
onClick={handleForcePoll}
title={t('cloud.checkQuota')}
disabled={forcePollMutation.isPending}
>
<RefreshCcw
className={`h-4 w-4 ${forcePollMutation.isPending ? 'animate-spin' : ''}`}
/>
</Button>
<Button
variant="outline"
onClick={handleSyncLocal}
disabled={syncMutation.isPending}
title={t('cloud.syncFromIDE')}
>
<Download
className={`mr-2 h-4 w-4 ${syncMutation.isPending ? 'animate-bounce' : ''}`}
/>
{t('cloud.syncFromIDE')}
</Button>
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" />
{t('cloud.addAccount')}
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>{t('cloud.authDialog.title')}</DialogTitle>
<DialogDescription>{t('cloud.authDialog.description')}</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Button variant="outline" className="col-span-4" onClick={openAuthUrl}>
<Cloud className="mr-2 h-4 w-4" />
{t('cloud.authDialog.openLogin')}
</Button>
</div>
<div className="space-y-2">
<Label htmlFor="code">{t('cloud.authDialog.authCode')}</Label>
<Input
id="code"
placeholder={t('cloud.authDialog.placeholder')}
value={authCode}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setAuthCode(e.target.value)
}
/>
<p className="text-muted-foreground text-xs">
{t('cloud.authDialog.instruction')}
</p>
</div>
</div>
<DialogFooter>
<Button
onClick={() => handleAddAccount()}
disabled={addMutation.isPending || !authCode}
>
{addMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{t('cloud.authDialog.verify')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<div className="flex flex-wrap items-center gap-2 rounded-lg border bg-card p-3">
<div className="bg-muted/50 flex items-center gap-2 rounded-md border px-3 py-2">
<div className="flex items-center gap-2">
<Zap
className={`h-4 w-4 ${autoSwitchEnabled ? 'fill-yellow-500 text-yellow-500' : 'text-muted-foreground'}`}
/>
<Label htmlFor="auto-switch" className="cursor-pointer text-sm font-medium">
{t('cloud.autoSwitch')}
</Label>
</div>
<Switch
id="auto-switch"
checked={!!autoSwitchEnabled}
onCheckedChange={handleToggleAutoSwitch}
disabled={isSettingsLoading || setAutoSwitchMutation.isPending}
/>
</div>
<Button variant="ghost" onClick={toggleSelectAll} title={t('cloud.batch.selectAll')} className="cursor-pointer">
<CheckSquare
className={`mr-2 h-4 w-4 ${selectedIds.size > 0 && selectedIds.size === accounts?.length ? 'text-primary fill-primary/20' : ''}`}
/>
{t('cloud.batch.selectAll')}
</Button>
<Button
variant="outline"
size="icon"
onClick={handleForcePoll}
title={t('cloud.checkQuota')}
disabled={forcePollMutation.isPending}
className="cursor-pointer"
>
<RefreshCcw
className={`h-4 w-4 ${forcePollMutation.isPending ? 'animate-spin' : ''}`}
/>
</Button>
<Button
variant="outline"
onClick={handleSyncLocal}
disabled={syncMutation.isPending}
title={t('cloud.syncFromIDE')}
className="cursor-pointer"
>
<Download
className={`mr-2 h-4 w-4 ${syncMutation.isPending ? 'animate-bounce' : ''}`}
/>
{t('cloud.syncFromIDE')}
</Button>
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogTrigger asChild>
<Button className="cursor-pointer">
<Plus className="mr-2 h-4 w-4" />
{t('cloud.addAccount')}
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>{t('cloud.authDialog.title')}</DialogTitle>
<DialogDescription>{t('cloud.authDialog.description')}</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Button variant="outline" className="col-span-4" onClick={openAuthUrl}>
<Cloud className="mr-2 h-4 w-4" />
{t('cloud.authDialog.openLogin')}
</Button>
</div>
<div className="space-y-2">
<Label htmlFor="code">{t('cloud.authDialog.authCode')}</Label>
<Input
id="code"
placeholder={t('cloud.authDialog.placeholder')}
value={authCode}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setAuthCode(e.target.value)
}
/>
<p className="text-muted-foreground text-xs">
{t('cloud.authDialog.instruction')}
</p>
</div>
</div>
<DialogFooter>
<Button
onClick={() => handleAddAccount()}
disabled={addMutation.isPending || !authCode}
>
{addMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{t('cloud.authDialog.verify')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{accounts?.map((account) => (
<CloudAccountCard
key={account.id}
@@ -380,6 +431,7 @@ export function CloudAccountList() {
onRefresh={handleRefresh}
onDelete={handleDelete}
onSwitch={handleSwitch}
onManageIdentity={handleManageIdentity}
isSelected={selectedIds.has(account.id)}
onToggleSelection={toggleSelection}
isRefreshing={
@@ -395,15 +447,16 @@ export function CloudAccountList() {
))}
{accounts?.length === 0 && (
<div className="text-muted-foreground bg-muted/10 col-span-full rounded-lg border-2 border-dashed py-12 text-center">
{t('cloud.list.noAccounts')}
<div className="text-muted-foreground bg-muted/20 col-span-full rounded-lg border border-dashed py-14 text-center">
<Cloud className="mx-auto mb-3 h-10 w-10 opacity-40" />
<div className="text-sm">{t('cloud.list.noAccounts')}</div>
</div>
)}
</div>
{/* Batch Action Bar */}
{selectedIds.size > 0 && (
<div className="bg-background animate-in fade-in slide-in-from-bottom-4 fixed bottom-6 left-1/2 z-50 flex -translate-x-1/2 items-center gap-4 rounded-full border px-6 py-2 shadow-xl">
<div className="bg-card animate-in fade-in slide-in-from-bottom-4 fixed bottom-6 left-1/2 z-50 flex -translate-x-1/2 items-center gap-4 rounded-full border px-6 py-2 shadow-lg">
<div className="flex items-center gap-2 border-r pr-4">
<span className="text-sm font-semibold">
{t('cloud.batch.selected', { count: selectedIds.size })}
@@ -430,6 +483,16 @@ export function CloudAccountList() {
</div>
</div>
)}
<IdentityProfileDialog
account={identityAccount}
open={Boolean(identityAccount)}
onOpenChange={(nextOpen) => {
if (!nextOpen) {
setIdentityAccount(null);
}
}}
/>
</div>
);
}
+428
View File
@@ -0,0 +1,428 @@
import { type JSX, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Fingerprint,
FolderOpen,
RotateCcw,
Wand2,
Trash2,
History,
ShieldCheck,
Cpu,
} from 'lucide-react';
import type { CloudAccount } from '@/types/cloudAccount';
import type { DeviceProfile, DeviceProfilesSnapshot, DeviceProfileVersion } from '@/types/account';
import {
bindCloudIdentityProfile,
bindCloudIdentityProfileWithPayload,
deleteCloudIdentityProfileRevision,
getCloudIdentityProfiles,
openCloudIdentityStorageFolder,
previewGenerateCloudIdentityProfile,
restoreCloudIdentityProfileRevision,
restoreCloudBaselineProfile,
} from '@/actions/cloud';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useToast } from '@/components/ui/use-toast';
interface IdentityProfileDialogProps {
account: CloudAccount | null;
open: boolean;
onOpenChange: (open: boolean) => void;
}
function formatError(error: unknown): string {
if (error instanceof Error && error.message) {
return error.message;
}
return String(error);
}
function renderProfile(profile?: DeviceProfile): JSX.Element {
if (!profile) {
return (
<div className="text-muted-foreground rounded-lg border border-dashed px-3 py-4 text-xs">
N/A
</div>
);
}
const rows: Array<{ label: string; value: string }> = [
{ label: 'machineId', value: profile.machineId },
{ label: 'macMachineId', value: profile.macMachineId },
{ label: 'devDeviceId', value: profile.devDeviceId },
{ label: 'sqmId', value: profile.sqmId },
];
return (
<div className="space-y-2">
{rows.map((row) => (
<div key={row.label} className="flex items-start justify-between gap-3">
<span className="text-muted-foreground text-[11px] tracking-wide uppercase">
{row.label}
</span>
<span className="max-w-[70%] text-right font-mono text-xs break-all">{row.value}</span>
</div>
))}
</div>
);
}
function sortHistory(history: DeviceProfileVersion[]): DeviceProfileVersion[] {
return [...history].sort((a, b) => b.createdAt - a.createdAt);
}
export function IdentityProfileDialog({ account, open, onOpenChange }: IdentityProfileDialogProps) {
const { t } = useTranslation();
const { toast } = useToast();
const [snapshot, setSnapshot] = useState<DeviceProfilesSnapshot | null>(null);
const [initialLoading, setInitialLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [actionKey, setActionKey] = useState<string | null>(null);
const [previewProfile, setPreviewProfile] = useState<DeviceProfile | null>(null);
const actionLockRef = useRef(false);
const history = useMemo(() => sortHistory(snapshot?.history || []), [snapshot?.history]);
const showLoadingPlaceholder = initialLoading && !snapshot;
const refreshProfiles = async (options?: { silent?: boolean }) => {
if (!account) {
setSnapshot(null);
return;
}
const silent = options?.silent === true;
if (silent) {
setRefreshing(true);
} else {
setInitialLoading(true);
}
try {
const result = await getCloudIdentityProfiles({ accountId: account.id });
setSnapshot(result);
} catch (error) {
toast({
title: t('cloud.toast.actionFailed', { defaultValue: 'Action failed' }),
description: formatError(error),
variant: 'destructive',
});
} finally {
if (silent) {
setRefreshing(false);
} else {
setInitialLoading(false);
}
}
};
useEffect(() => {
if (!open) {
return;
}
refreshProfiles();
}, [open, account?.id]);
const runAction = async (key: string, action: () => Promise<void>) => {
if (actionLockRef.current) {
return;
}
actionLockRef.current = true;
setActionKey(key);
try {
await action();
await refreshProfiles({ silent: true });
} catch (error) {
toast({
title: t('cloud.toast.actionFailed', { defaultValue: 'Action failed' }),
description: formatError(error),
variant: 'destructive',
});
} finally {
actionLockRef.current = false;
setActionKey(null);
}
};
if (!account) {
return null;
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="flex max-h-[90vh] max-w-6xl flex-col overflow-hidden p-0">
<DialogHeader className="from-primary/10 via-background to-background border-b bg-gradient-to-r p-6">
<div className="flex items-start justify-between gap-4">
<div className="flex min-w-0 items-center gap-3">
<div className="bg-primary/15 text-primary rounded-xl border p-2.5">
<Fingerprint className="h-5 w-5" />
</div>
<div className="min-w-0">
<DialogTitle className="truncate">{t('cloud.identity.title')}</DialogTitle>
<DialogDescription className="truncate">{account.email}</DialogDescription>
</div>
</div>
<Badge variant="secondary" className="shrink-0">
<History className="mr-1.5 h-3.5 w-3.5" />
{history.length}
</Badge>
</div>
</DialogHeader>
<div className="flex-1 space-y-6 overflow-y-auto p-6" aria-busy={refreshing}>
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
<Button
variant="outline"
disabled={actionKey === 'preview-generate'}
className="h-auto cursor-pointer justify-start rounded-xl px-4 py-3"
onClick={() => {
runAction('preview-generate', async () => {
const profile = await previewGenerateCloudIdentityProfile();
setPreviewProfile(profile);
});
}}
>
<Wand2 className="mr-2 h-4 w-4" />
{t('cloud.identity.generateAndBind')}
</Button>
<Button
variant="outline"
disabled={actionKey === 'capture-bind'}
className="h-auto cursor-pointer justify-start rounded-xl px-4 py-3"
onClick={() => {
runAction('capture-bind', async () => {
await bindCloudIdentityProfile({ accountId: account.id, mode: 'capture' });
setPreviewProfile(null);
toast({ title: t('cloud.identity.captureSuccess') });
});
}}
>
<Fingerprint className="mr-2 h-4 w-4" />
{t('cloud.identity.captureAndBind')}
</Button>
<Button
variant="outline"
disabled={actionKey === 'restore-original'}
className="h-auto cursor-pointer justify-start rounded-xl px-4 py-3"
onClick={() => {
runAction('restore-original', async () => {
await restoreCloudBaselineProfile({ accountId: account.id });
toast({ title: t('cloud.identity.restoreOriginalSuccess') });
});
}}
>
<RotateCcw className="mr-2 h-4 w-4" />
{t('cloud.identity.restoreOriginal')}
</Button>
<Button
variant="outline"
disabled={actionKey === 'open-folder'}
className="h-auto cursor-pointer justify-start rounded-xl px-4 py-3"
onClick={() => {
runAction('open-folder', async () => {
await openCloudIdentityStorageFolder();
toast({ title: t('cloud.identity.openFolderSuccess') });
});
}}
>
<FolderOpen className="mr-2 h-4 w-4" />
{t('cloud.identity.openFolder')}
</Button>
</div>
{previewProfile ? (
<Card className="border-primary/20 shadow-sm">
<CardHeader className="pb-3">
<CardTitle className="text-sm">{t('cloud.identity.previewTitle')}</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{renderProfile(previewProfile)}
<div className="flex flex-wrap gap-2">
<Button
size="sm"
disabled={actionKey === 'confirm-generate'}
className="cursor-pointer"
onClick={() => {
runAction('confirm-generate', async () => {
await bindCloudIdentityProfileWithPayload({
accountId: account.id,
profile: previewProfile,
});
setPreviewProfile(null);
toast({ title: t('cloud.identity.generateSuccess') });
});
}}
>
{t('cloud.identity.confirm')}
</Button>
<Button
size="sm"
variant="ghost"
className="cursor-pointer"
onClick={() => setPreviewProfile(null)}
>
{t('cloud.identity.cancel')}
</Button>
</div>
</CardContent>
</Card>
) : null}
<div className="grid gap-6 xl:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)]">
<div className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<Card className="shadow-sm">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-sm">
<Cpu className="text-muted-foreground h-4 w-4" />
{t('cloud.identity.currentStorage')}
</CardTitle>
</CardHeader>
<CardContent>
{showLoadingPlaceholder ? (
<div className="text-muted-foreground text-xs">
{t('cloud.identity.loading')}
</div>
) : (
renderProfile(snapshot?.currentStorage)
)}
</CardContent>
</Card>
<Card className="shadow-sm">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-sm">
<ShieldCheck className="text-muted-foreground h-4 w-4" />
{t('cloud.identity.accountBinding')}
</CardTitle>
</CardHeader>
<CardContent>
{showLoadingPlaceholder ? (
<div className="text-muted-foreground text-xs">
{t('cloud.identity.loading')}
</div>
) : (
renderProfile(snapshot?.boundProfile)
)}
</CardContent>
</Card>
</div>
<Card className="shadow-sm">
<CardHeader className="pb-3">
<CardTitle className="text-sm">{t('cloud.identity.history')}</CardTitle>
</CardHeader>
<CardContent className="max-h-[38vh] space-y-3 overflow-y-auto pr-1">
{showLoadingPlaceholder ? (
<div className="text-muted-foreground text-xs">
{t('cloud.identity.loading')}
</div>
) : null}
{!showLoadingPlaceholder && history.length === 0 ? (
<div className="text-muted-foreground text-xs">
{t('cloud.identity.noHistory')}
</div>
) : null}
{!showLoadingPlaceholder
? history.map((version) => (
<div key={version.id} className="bg-muted/20 rounded-xl border p-3">
<div className="mb-2 flex items-center justify-between gap-3">
<div className="min-w-0">
<div className="truncate text-sm font-medium">{version.label}</div>
<div className="text-muted-foreground text-xs">
{new Date(version.createdAt * 1000).toLocaleString()}
</div>
</div>
{version.isCurrent ? (
<Badge variant="secondary" className="shrink-0">
{t('cloud.identity.current')}
</Badge>
) : null}
</div>
{renderProfile(version.profile)}
<div className="mt-3 flex flex-wrap gap-2">
<Button
size="sm"
variant="outline"
disabled={version.isCurrent || actionKey === `restore-${version.id}`}
className="cursor-pointer"
onClick={() => {
runAction(`restore-${version.id}`, async () => {
await restoreCloudIdentityProfileRevision({
accountId: account.id,
versionId: version.id,
});
toast({ title: t('cloud.identity.restoreVersionSuccess') });
});
}}
>
{t('cloud.identity.restore')}
</Button>
{!version.isCurrent ? (
<Button
size="sm"
variant="outline"
disabled={actionKey === `delete-${version.id}`}
className="cursor-pointer"
onClick={() => {
runAction(`delete-${version.id}`, async () => {
await deleteCloudIdentityProfileRevision({
accountId: account.id,
versionId: version.id,
});
toast({ title: t('cloud.identity.deleteVersionSuccess') });
});
}}
>
<Trash2 className="h-4 w-4" />
</Button>
) : null}
</div>
</div>
))
: null}
</CardContent>
</Card>
</div>
<Card className="shadow-sm">
<CardHeader className="pb-3">
<CardTitle className="text-sm">
{t('cloud.identity.baseline', { defaultValue: 'Reference Profile' })}
</CardTitle>
</CardHeader>
<CardContent>
{showLoadingPlaceholder ? (
<div className="text-muted-foreground text-xs">{t('cloud.identity.loading')}</div>
) : (
renderProfile(snapshot?.baseline)
)}
</CardContent>
</Card>
</div>
</div>
<DialogFooter className="border-t px-6 py-4">
<Button variant="ghost" className="cursor-pointer" onClick={() => onOpenChange(false)}>
{t('cloud.identity.close')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+88 -35
View File
@@ -1,21 +1,22 @@
import React from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
isProcessRunning,
startAntigravity,
closeAntigravity,
} from "@/actions/process";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { Play, Square, Loader2 } from "lucide-react";
import { useTranslation } from "react-i18next";
import React from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { isProcessRunning, startAntigravity, closeAntigravity } from '@/actions/process';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { Play, Square, Loader2, Power } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
export const StatusBar: React.FC = () => {
interface StatusBarProps {
isCollapsed?: boolean;
}
export const StatusBar: React.FC<StatusBarProps> = ({ isCollapsed = false }) => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const { data: isRunning, isLoading } = useQuery({
queryKey: ["process", "status"],
queryKey: ['process', 'status'],
queryFn: isProcessRunning,
refetchInterval: 2000,
});
@@ -23,14 +24,14 @@ export const StatusBar: React.FC = () => {
const startMutation = useMutation({
mutationFn: startAntigravity,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["process", "status"] });
queryClient.invalidateQueries({ queryKey: ['process', 'status'] });
},
});
const stopMutation = useMutation({
mutationFn: closeAntigravity,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["process", "status"] });
queryClient.invalidateQueries({ queryKey: ['process', 'status'] });
},
});
@@ -44,30 +45,78 @@ export const StatusBar: React.FC = () => {
const isPending = startMutation.isPending || stopMutation.isPending;
// Collapsed View
if (isCollapsed) {
return (
<div className="flex justify-center py-2">
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={handleToggle}
disabled={isLoading || isPending}
className={cn(
'h-10 w-10 rounded-lg transition-all duration-300',
isRunning
? 'bg-green-100/50 text-green-700 hover:bg-green-100 hover:text-green-800 dark:bg-green-900/30 dark:text-green-400 dark:hover:bg-green-900/50'
: 'bg-red-100/50 text-red-700 hover:bg-red-100 hover:text-red-800 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50',
)}
>
{isPending ? (
<Loader2 className="h-5 w-5 animate-spin" />
) : (
<Power className="h-5 w-5" />
)}
</Button>
</TooltipTrigger>
<TooltipContent side="right" className="flex items-center gap-2">
<div
className={cn('h-2 w-2 rounded-full', isRunning ? 'bg-green-500' : 'bg-red-500')}
/>
<p>
{isLoading
? t('status.checking')
: isRunning
? t('status.running')
: t('status.stopped')}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
);
}
// Expanded View
return (
<div
className={cn(
"flex items-center justify-between px-4 py-2 text-sm font-medium transition-colors",
'flex items-center justify-between overflow-hidden rounded-lg px-3 py-2.5 text-sm font-medium ring-1 transition-colors ring-inset',
isRunning
? "bg-green-100 text-green-900 dark:bg-green-900/30 dark:text-green-100"
: "bg-red-100 text-red-900 dark:bg-red-900/30 dark:text-red-100",
? 'bg-green-100 text-green-900 ring-green-200 dark:bg-green-900/20 dark:text-green-100 dark:ring-green-900/50'
: 'bg-red-100 text-red-900 ring-red-200 dark:bg-red-900/20 dark:text-red-100 dark:ring-red-900/50',
)}
>
<div className="flex items-center gap-2">
<div className="flex flex-1 items-start gap-3">
{isLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
<Loader2 className="mt-0.5 h-3.5 w-3.5 shrink-0 animate-spin" />
) : isRunning ? (
<div className="h-3 w-3 rounded-full bg-green-500" />
<div className="mt-1.5 h-2.5 w-2.5 shrink-0 rounded-full bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.5)]" />
) : (
<div className="h-3 w-3 rounded-full bg-red-500" />
<div className="mt-1.5 h-2.5 w-2.5 shrink-0 rounded-full bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.5)]" />
)}
<span>
{isLoading
? t("status.checking")
: isRunning
? t("status.running")
: t("status.stopped")}
</span>
<div className="flex flex-col gap-1 leading-none">
<span className="text-xs font-semibold tracking-wider uppercase opacity-80">Status</span>
<span className="text-sm leading-tight font-medium">
{isLoading
? t('status.checking')
: isRunning
? t('status.running')
: t('status.stopped')}
</span>
</div>
</div>
<Button
variant="ghost"
@@ -75,18 +124,22 @@ export const StatusBar: React.FC = () => {
onClick={handleToggle}
disabled={isLoading || isPending}
className={cn(
"h-8 px-3 hover:bg-white/20",
isRunning ? "hover:text-green-900" : "hover:text-red-900",
'ml-2 h-8 shrink-0 rounded-md border px-3 transition-all',
isRunning
? 'border-green-200 bg-green-200/50 text-green-900 hover:bg-green-200 dark:border-green-800 dark:bg-green-800/30 dark:text-green-100 dark:hover:bg-green-800/50'
: 'border-red-200 bg-red-200/50 text-red-900 hover:bg-red-200 dark:border-red-800 dark:bg-red-800/30 dark:text-red-100 dark:hover:bg-red-800/50',
)}
>
{isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : isRunning ? (
<Square className="mr-2 h-4 w-4 fill-current" />
<Square className="h-3.5 w-3.5 fill-current" />
) : (
<Play className="mr-2 h-4 w-4 fill-current" />
<Play className="h-3.5 w-3.5 fill-current" />
)}
{isRunning ? t("action.stop") : t("action.start")}
<span className="ml-2 font-semibold">
{isRunning ? t('action.stop') : t('action.start')}
</span>
</Button>
</div>
);
+4 -4
View File
@@ -1,12 +1,12 @@
import { Link } from "@tanstack/react-router";
import { Link } from '@tanstack/react-router';
import {
NavigationMenu as NavigationMenuBase,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu";
import { useTranslation } from "react-i18next";
} from '@/components/ui/navigation-menu';
import { useTranslation } from 'react-i18next';
export default function NavigationMenu() {
const { t } = useTranslation();
@@ -16,7 +16,7 @@ export default function NavigationMenu() {
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
<Link to="/">{t("titleHomePage")}</Link>
<Link to="/">{t('titleHomePage')}</Link>
</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList>
+3 -3
View File
@@ -1,6 +1,6 @@
import { Moon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { toggleTheme } from "@/actions/theme";
import { Moon } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { toggleTheme } from '@/actions/theme';
export default function ToggleTheme() {
return (
+6 -9
View File
@@ -1,6 +1,6 @@
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@/lib/utils";
import * as React from 'react';
import * as AvatarPrimitive from '@radix-ui/react-avatar';
import { cn } from '@/lib/utils';
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
@@ -8,10 +8,7 @@ const Avatar = React.forwardRef<
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className,
)}
className={cn('relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full', className)}
{...props}
/>
));
@@ -23,7 +20,7 @@ const AvatarImage = React.forwardRef<
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
className={cn('aspect-square h-full w-full', className)}
{...props}
/>
));
@@ -36,7 +33,7 @@ const AvatarFallback = React.forwardRef<
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"bg-muted flex h-full w-full items-center justify-center rounded-full",
'bg-muted flex h-full w-full items-center justify-center rounded-full',
className,
)}
{...props}
Loaded 100 of 232 files, more files were not shown because too many files have changed in this diff. Show more