Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89f39710c7 | ||
|
|
1bce5d331d | ||
|
|
47be175e27 | ||
|
|
f0718db374 | ||
|
|
4b12f1220d | ||
|
|
47c14763de | ||
|
|
37a9750df2 | ||
|
|
f708de24c0 | ||
|
|
6e7f2252ad | ||
|
|
e96d3e0e8e | ||
|
|
12ab4e0ce7 | ||
|
|
013f3290f3 | ||
|
|
f224fea081 | ||
|
|
a438070268 | ||
|
|
e927ac9506 |
No files matched your search
@@ -11,7 +11,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [windows-latest, macos-latest, ubuntu-latest]
|
os: [windows-latest, macos-latest, macos-14, ubuntu-latest]
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
|||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit"
|
||||||
|
},
|
||||||
|
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"]
|
||||||
|
}
|
||||||
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [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
|
## [0.2.2] - 2026-01-19
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
+1
-1
@@ -93,7 +93,7 @@ src/
|
|||||||
**数据库位置:**
|
**数据库位置:**
|
||||||
* macOS: `~/Library/Application Support/Antigravity/User/globalStorage/state.vscdb`
|
* macOS: `~/Library/Application Support/Antigravity/User/globalStorage/state.vscdb`
|
||||||
* Windows: `%APPDATA%/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`: 认证状态和用户信息。
|
* `antigravityAuthStatus`: 认证状态和用户信息。
|
||||||
|
|||||||
Generated
+39
-5
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "antigravity-manager",
|
"name": "antigravity-manager",
|
||||||
"version": "0.2.2",
|
"version": "0.3.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "antigravity-manager",
|
"name": "antigravity-manager",
|
||||||
"version": "0.2.2",
|
"version": "0.3.1",
|
||||||
"license": "CC-BY-NC-SA-4.0",
|
"license": "CC-BY-NC-SA-4.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grpc/grpc-js": "^1.14.3",
|
"@grpc/grpc-js": "^1.14.3",
|
||||||
@@ -48,6 +48,7 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
"fastify": "^5.6.2",
|
"fastify": "^5.6.2",
|
||||||
|
"find-process": "^2.0.0",
|
||||||
"happy-dom": "^20.0.10",
|
"happy-dom": "^20.0.10",
|
||||||
"i18next": "^25.6.3",
|
"i18next": "^25.6.3",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
@@ -10188,7 +10189,6 @@
|
|||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-styles": "^4.1.0",
|
"ansi-styles": "^4.1.0",
|
||||||
@@ -13074,6 +13074,29 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/find-process": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/find-process/-/find-process-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-YUBQnteWGASJoEVVsOXy6XtKAY2O1FCsWnnvQ8y0YwgY1rZiKeVptnFvMu6RSELZAJOGklqseTnUGGs5D0bKmg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "~4.1.2",
|
||||||
|
"commander": "^12.1.0",
|
||||||
|
"loglevel": "^1.9.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"find-process": "dist/bin/find-process.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/find-process/node_modules/commander": {
|
||||||
|
"version": "12.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||||
|
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/find-up": {
|
"node_modules/find-up": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
||||||
@@ -13713,7 +13736,6 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -16063,6 +16085,19 @@
|
|||||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/loglevel": {
|
||||||
|
"version": "1.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz",
|
||||||
|
"integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/loglevel"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/long": {
|
"node_modules/long": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||||
@@ -19963,7 +19998,6 @@
|
|||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-flag": "^4.0.0"
|
"has-flag": "^4.0.0"
|
||||||
|
|||||||
+2
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "antigravity-manager",
|
"name": "antigravity-manager",
|
||||||
"productName": "Antigravity Manager",
|
"productName": "Antigravity Manager",
|
||||||
"version": "0.2.2",
|
"version": "0.3.1",
|
||||||
"description": "Antigravity Manager - Electron App",
|
"description": "Antigravity Manager - Electron App",
|
||||||
"main": ".vite/build/main.js",
|
"main": ".vite/build/main.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -116,6 +116,7 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
"fastify": "^5.6.2",
|
"fastify": "^5.6.2",
|
||||||
|
"find-process": "^2.0.0",
|
||||||
"happy-dom": "^20.0.10",
|
"happy-dom": "^20.0.10",
|
||||||
"i18next": "^25.6.3",
|
"i18next": "^25.6.3",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { ipc } from '@/ipc/manager';
|
||||||
|
|
||||||
|
export function openLogDirectory() {
|
||||||
|
return ipc.client.system.openLogDirectory();
|
||||||
|
}
|
||||||
@@ -62,19 +62,49 @@ export function CloudAccountList() {
|
|||||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
||||||
const [authCode, setAuthCode] = useState('');
|
const [authCode, setAuthCode] = useState('');
|
||||||
|
|
||||||
|
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',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
// Listen for Google Auth Code
|
// Listen for Google Auth Code
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (window.electron?.onGoogleAuthCode) {
|
if (window.electron?.onGoogleAuthCode) {
|
||||||
|
console.log('[OAuth] Setting up auth code listener, dialog open:', isAddDialogOpen);
|
||||||
const cleanup = window.electron.onGoogleAuthCode((code) => {
|
const cleanup = window.electron.onGoogleAuthCode((code) => {
|
||||||
|
console.log('[OAuth] Received auth code via IPC:', code?.substring(0, 10) + '...');
|
||||||
setAuthCode(code);
|
setAuthCode(code);
|
||||||
// Auto submit if dialog is open
|
// Note: Auto-submit will be triggered by the authCode change effect below
|
||||||
if (isAddDialogOpen) {
|
|
||||||
handleAddAccount(code);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return cleanup;
|
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
|
// Batch Operations State
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||||
@@ -176,28 +206,6 @@ export function CloudAccountList() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
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 () => {
|
const openAuthUrl = async () => {
|
||||||
try {
|
try {
|
||||||
await startAuthFlow();
|
await startAuthFlow();
|
||||||
|
|||||||
@@ -20,11 +20,13 @@ export class AuthServer {
|
|||||||
const error = url.searchParams.get('error');
|
const error = url.searchParams.get('error');
|
||||||
|
|
||||||
if (code) {
|
if (code) {
|
||||||
logger.info('AuthServer: Received authorization code');
|
logger.info(`AuthServer: Received authorization code: ${code.substring(0, 10)}...`);
|
||||||
|
|
||||||
// Send code to renderer
|
// Send code to renderer
|
||||||
if (ipcContext.mainWindow) {
|
if (ipcContext.mainWindow) {
|
||||||
|
logger.info('AuthServer: Sending code to renderer via IPC');
|
||||||
ipcContext.mainWindow.webContents.send('GOOGLE_AUTH_CODE', code);
|
ipcContext.mainWindow.webContents.send('GOOGLE_AUTH_CODE', code);
|
||||||
|
logger.info('AuthServer: Code sent successfully');
|
||||||
} else {
|
} else {
|
||||||
logger.error('AuthServer: Main window not found, cannot send code');
|
logger.error('AuthServer: Main window not found, cannot send code');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ function ensureDatabaseInitialized(dbPath: string): void {
|
|||||||
let db: Database.Database | null = null;
|
let db: Database.Database | null = null;
|
||||||
try {
|
try {
|
||||||
db = new Database(dbPath);
|
db = new Database(dbPath);
|
||||||
|
db.pragma('journal_mode = WAL');
|
||||||
|
|
||||||
// Create accounts table
|
// Create accounts table
|
||||||
// Storing complex objects (token, quota) as JSON strings for simplicity
|
// Storing complex objects (token, quota) as JSON strings for simplicity
|
||||||
@@ -73,7 +74,9 @@ function ensureDatabaseInitialized(dbPath: string): void {
|
|||||||
function getDb(): Database.Database {
|
function getDb(): Database.Database {
|
||||||
const dbPath = getCloudAccountsDbPath();
|
const dbPath = getCloudAccountsDbPath();
|
||||||
ensureDatabaseInitialized(dbPath);
|
ensureDatabaseInitialized(dbPath);
|
||||||
return new Database(dbPath);
|
const db = new Database(dbPath);
|
||||||
|
db.pragma('journal_mode = WAL');
|
||||||
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CloudAccountRepo {
|
export class CloudAccountRepo {
|
||||||
@@ -307,6 +310,7 @@ export class CloudAccountRepo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const db = new Database(dbPath);
|
const db = new Database(dbPath);
|
||||||
|
db.pragma('journal_mode = WAL');
|
||||||
try {
|
try {
|
||||||
const row = db
|
const row = db
|
||||||
.prepare('SELECT value FROM ItemTable WHERE key = ?')
|
.prepare('SELECT value FROM ItemTable WHERE key = ?')
|
||||||
@@ -454,6 +458,7 @@ export class CloudAccountRepo {
|
|||||||
|
|
||||||
logger.info(`SyncLocal: Using Antigravity database at: ${dbPath}`);
|
logger.info(`SyncLocal: Using Antigravity database at: ${dbPath}`);
|
||||||
const ideDb = new Database(dbPath, { readonly: true });
|
const ideDb = new Database(dbPath, { readonly: true });
|
||||||
|
ideDb.pragma('journal_mode = WAL');
|
||||||
try {
|
try {
|
||||||
// 1. Read Raw Token Data
|
// 1. Read Raw Token Data
|
||||||
const row = ideDb
|
const row = ideDb
|
||||||
|
|||||||
@@ -8,6 +8,31 @@ import path from 'path';
|
|||||||
|
|
||||||
const KEYS_TO_BACKUP = ['antigravityAuthStatus', 'jetskiStateSync.agentManagerInitState'];
|
const KEYS_TO_BACKUP = ['antigravityAuthStatus', 'jetskiStateSync.agentManagerInitState'];
|
||||||
|
|
||||||
|
function getDbWithWal(dbPath: string, options: Database.Options = {}): Database.Database {
|
||||||
|
const db = new Database(dbPath, options);
|
||||||
|
db.pragma('journal_mode = WAL');
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the database and ensures WAL mode is enabled.
|
||||||
|
* Should be called on application startup.
|
||||||
|
*/
|
||||||
|
export function initDatabase(): void {
|
||||||
|
try {
|
||||||
|
const dbPaths = getAntigravityDbPaths();
|
||||||
|
if (dbPaths.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = getDatabaseConnection();
|
||||||
|
db.close();
|
||||||
|
logger.info('Database initialized and verified (WAL mode)');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to initialize database on startup', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensures that the database file exists.
|
* Ensures that the database file exists.
|
||||||
* @param dbPath {string} The path to the database file.
|
* @param dbPath {string} The path to the database file.
|
||||||
@@ -59,7 +84,7 @@ export function getDatabaseConnection(dbPath?: string): Database.Database {
|
|||||||
ensureDatabaseExists(targetPath);
|
ensureDatabaseExists(targetPath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return new Database(targetPath, { readonly: false, fileMustExist: false });
|
return getDbWithWal(targetPath, { readonly: false, fileMustExist: false });
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const err = error as { code?: string };
|
const err = error as { code?: string };
|
||||||
if (err.code === 'SQLITE_BUSY' || err.code === 'SQLITE_LOCKED') {
|
if (err.code === 'SQLITE_BUSY' || err.code === 'SQLITE_LOCKED') {
|
||||||
@@ -69,6 +94,12 @@ export function getDatabaseConnection(dbPath?: string): Database.Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a database connection and enables WAL mode.
|
||||||
|
* @param dbPath {string} The path to the database file.
|
||||||
|
* @returns {Database.Database} The database connection.
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the current account info.
|
* Gets the current account info.
|
||||||
* @returns {AccountInfo} The current account info.
|
* @returns {AccountInfo} The current account info.
|
||||||
|
|||||||
+106
-38
@@ -1,63 +1,131 @@
|
|||||||
import { exec, execSync } from 'child_process';
|
import { exec, execSync } from 'child_process';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
|
import findProcess from 'find-process';
|
||||||
import { getAntigravityExecutablePath, isWsl } from '../../utils/paths';
|
import { getAntigravityExecutablePath, isWsl } from '../../utils/paths';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper process name patterns to exclude (Electron helper processes)
|
||||||
|
*/
|
||||||
|
const HELPER_PATTERNS = [
|
||||||
|
'helper',
|
||||||
|
'plugin',
|
||||||
|
'renderer',
|
||||||
|
'gpu',
|
||||||
|
'crashpad',
|
||||||
|
'utility',
|
||||||
|
'audio',
|
||||||
|
'sandbox',
|
||||||
|
'language_server',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a process is a helper/auxiliary process that should be excluded.
|
||||||
|
* @param name Process name (lowercase)
|
||||||
|
* @param cmd Process command line (lowercase)
|
||||||
|
* @returns True if the process is a helper process
|
||||||
|
*/
|
||||||
|
function isHelperProcess(name: string, cmd: string): boolean {
|
||||||
|
const nameLower = name.toLowerCase();
|
||||||
|
const cmdLower = cmd.toLowerCase();
|
||||||
|
|
||||||
|
// Check for --type= argument (Electron helper process indicator)
|
||||||
|
if (cmdLower.includes('--type=')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for helper patterns in process name
|
||||||
|
for (const pattern of HELPER_PATTERNS) {
|
||||||
|
if (nameLower.includes(pattern)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for crashpad in path
|
||||||
|
if (cmdLower.includes('crashpad')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the Antigravity process is running.
|
* Checks if the Antigravity process is running.
|
||||||
|
* Uses find-process package for robust cross-platform process detection.
|
||||||
* @returns {boolean} True if the Antigravity process is running, false otherwise.
|
* @returns {boolean} True if the Antigravity process is running, false otherwise.
|
||||||
*/
|
*/
|
||||||
export async function isProcessRunning(): Promise<boolean> {
|
export async function isProcessRunning(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const platform = process.platform;
|
const platform = process.platform;
|
||||||
let command = '';
|
const currentPid = process.pid;
|
||||||
|
|
||||||
if (platform === 'win32' || isWsl()) {
|
// Use find-process to search for Antigravity processes
|
||||||
// Use absolute path for tasklist to be safe on Windows too
|
// 'name' search type matches process name
|
||||||
const cmd = isWsl()
|
const processes = await findProcess('name', 'Antigravity', true);
|
||||||
? '/mnt/c/Windows/System32/tasklist.exe'
|
|
||||||
: process.env.SystemRoot // Usually C:\Windows
|
|
||||||
? `${process.env.SystemRoot}\\System32\\tasklist.exe`
|
|
||||||
: 'tasklist';
|
|
||||||
|
|
||||||
command = `${cmd} /FI "IMAGENAME eq Antigravity.exe" /NH`;
|
logger.debug(`Found ${processes.length} processes matching 'Antigravity'`);
|
||||||
} else {
|
|
||||||
// macOS and Linux (native)
|
for (const proc of processes) {
|
||||||
command = 'pgrep -xi Antigravity';
|
// Skip self
|
||||||
|
if (proc.pid === currentPid) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`Checking process with command: ${command}`);
|
const name = proc.name?.toLowerCase() || '';
|
||||||
// Increased timeout to 4000ms to avoid flakiness with tasklist/wmic
|
const cmd = proc.cmd?.toLowerCase() || '';
|
||||||
try {
|
|
||||||
const { stdout } = await execAsync(command, { timeout: 4000 });
|
|
||||||
logger.debug(`Process check stdout: ${stdout.trim()}`);
|
|
||||||
|
|
||||||
if (platform === 'win32' || isWsl()) {
|
// Skip manager process
|
||||||
return stdout.includes('Antigravity.exe');
|
if (
|
||||||
} else {
|
name.includes('manager') ||
|
||||||
// pgrep returns PIDs if found, empty if not
|
cmd.includes('manager') ||
|
||||||
return stdout.trim().length > 0;
|
cmd.includes('antigravity-manager')
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
} catch (cmdError) {
|
|
||||||
if (platform === 'win32' && !isWsl()) {
|
// Skip helper processes
|
||||||
logger.warn('tasklist failed, trying wmic fallback...', cmdError);
|
if (isHelperProcess(name, cmd)) {
|
||||||
// Fallback to wmic (filtered)
|
continue;
|
||||||
try {
|
}
|
||||||
const wmicCmd = 'wmic process where "name=\'Antigravity.exe\'" get ProcessId';
|
|
||||||
const { stdout } = await execAsync(wmicCmd, { timeout: 4000 });
|
if (platform === 'darwin') {
|
||||||
return stdout.includes('ProcessId');
|
// macOS: Check for Antigravity.app in path
|
||||||
} catch (wmicError) {
|
if (cmd.includes('antigravity.app')) {
|
||||||
logger.error('wmic fallback also failed', wmicError);
|
logger.debug(
|
||||||
|
`Found Antigravity process: PID=${proc.pid}, name=${name}, cmd=${cmd.substring(0, 100)}`,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Also check if the process name is exactly 'Antigravity' (main process)
|
||||||
|
if (name === 'antigravity' && !isHelperProcess(name, cmd)) {
|
||||||
|
logger.debug(`Found Antigravity process: PID=${proc.pid}, name=${name}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (platform === 'win32') {
|
||||||
|
// Windows: Check for Antigravity.exe
|
||||||
|
if (name === 'antigravity.exe' || name === 'antigravity') {
|
||||||
|
logger.debug(`Found Antigravity process: PID=${proc.pid}, name=${name}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Linux: Check for antigravity in name or path (but not tools)
|
||||||
|
if (
|
||||||
|
(name.includes('antigravity') || cmd.includes('/antigravity')) &&
|
||||||
|
!name.includes('tools')
|
||||||
|
) {
|
||||||
|
logger.debug(
|
||||||
|
`Found Antigravity process: PID=${proc.pid}, name=${name}, cmd=${cmd.substring(0, 100)}`,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
}
|
|
||||||
throw cmdError;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error checking process status:', error);
|
logger.error('Error checking process status with find-process:', error);
|
||||||
// pgrep returns exit code 1 if no process found
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { os } from '@orpc/server';
|
import { os } from '@orpc/server';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { networkInterfaces } from 'os';
|
import { networkInterfaces } from 'os';
|
||||||
|
import { shell } from 'electron';
|
||||||
|
import { getAgentDir } from '../../utils/paths';
|
||||||
|
|
||||||
// Schema for IP info
|
// Schema for IP info
|
||||||
const IpInfoSchema = z.object({
|
const IpInfoSchema = z.object({
|
||||||
@@ -61,4 +63,10 @@ export const systemHandler = os.router({
|
|||||||
|
|
||||||
return results;
|
return results;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// Open log directory in file explorer
|
||||||
|
openLogDirectory: os.output(z.void()).handler(async () => {
|
||||||
|
const logDir = getAgentDir();
|
||||||
|
await shell.openPath(logDir);
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
@@ -30,6 +30,7 @@ i18n
|
|||||||
deleteBackup: 'Delete Backup',
|
deleteBackup: 'Delete Backup',
|
||||||
backupCurrent: 'Backup Current',
|
backupCurrent: 'Backup Current',
|
||||||
retry: 'Retry',
|
retry: 'Retry',
|
||||||
|
openLogs: 'Open Log Directory',
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
generic: 'An unexpected error occurred.',
|
generic: 'An unexpected error occurred.',
|
||||||
@@ -77,6 +78,7 @@ i18n
|
|||||||
version: 'Version',
|
version: 'Version',
|
||||||
platform: 'Platform',
|
platform: 'Platform',
|
||||||
license: 'License',
|
license: 'License',
|
||||||
|
openLogDir: 'Open',
|
||||||
account: {
|
account: {
|
||||||
title: 'Account Settings',
|
title: 'Account Settings',
|
||||||
description: 'Configure automatic account refresh and sync.',
|
description: 'Configure automatic account refresh and sync.',
|
||||||
@@ -304,6 +306,7 @@ i18n
|
|||||||
deleteBackup: '删除备份',
|
deleteBackup: '删除备份',
|
||||||
backupCurrent: '备份当前账号',
|
backupCurrent: '备份当前账号',
|
||||||
retry: '重试',
|
retry: '重试',
|
||||||
|
openLogs: '打开日志目录',
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
generic: '发生未知错误。',
|
generic: '发生未知错误。',
|
||||||
@@ -351,6 +354,7 @@ i18n
|
|||||||
version: '版本',
|
version: '版本',
|
||||||
platform: '平台',
|
platform: '平台',
|
||||||
license: '许可证',
|
license: '许可证',
|
||||||
|
openLogDir: '打开',
|
||||||
account: {
|
account: {
|
||||||
title: '账号设置',
|
title: '账号设置',
|
||||||
description: '配置自动刷新和同步账号。',
|
description: '配置自动刷新和同步账号。',
|
||||||
|
|||||||
+15
-4
@@ -1,4 +1,4 @@
|
|||||||
import { app, BrowserWindow, Tray, Menu, nativeImage } from 'electron';
|
import { app, BrowserWindow } from 'electron';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
@@ -9,6 +9,7 @@ import { IPC_CHANNELS } from './constants';
|
|||||||
import { updateElectronApp, UpdateSourceType } from 'update-electron-app';
|
import { updateElectronApp, UpdateSourceType } from 'update-electron-app';
|
||||||
import { logger } from './utils/logger';
|
import { logger } from './utils/logger';
|
||||||
import { CloudAccountRepo } from './ipc/database/cloudHandler';
|
import { CloudAccountRepo } from './ipc/database/cloudHandler';
|
||||||
|
import { initDatabase } from './ipc/database/handler';
|
||||||
import { CloudMonitorService } from './services/CloudMonitorService';
|
import { CloudMonitorService } from './services/CloudMonitorService';
|
||||||
|
|
||||||
// Static Imports to fix Bundle Resolution Errors
|
// Static Imports to fix Bundle Resolution Errors
|
||||||
@@ -223,6 +224,19 @@ process.on('unhandledRejection', (reason) => {
|
|||||||
|
|
||||||
app
|
app
|
||||||
.whenReady()
|
.whenReady()
|
||||||
|
.then(async () => {
|
||||||
|
logger.info('Step: Initialize CloudAccountRepo');
|
||||||
|
try {
|
||||||
|
await CloudAccountRepo.init();
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Startup: Failed to initialize CloudAccountRepo', e);
|
||||||
|
// We might want to exit here or show a dialog, but for now we proceed
|
||||||
|
// though functionality will be broken.
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Step: Initialize Antigravity DB (WAL Mode)');
|
||||||
|
initDatabase();
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info('Step: setupORPC');
|
logger.info('Step: setupORPC');
|
||||||
return setupORPC();
|
return setupORPC();
|
||||||
@@ -242,9 +256,6 @@ app
|
|||||||
.then(async () => {
|
.then(async () => {
|
||||||
// Initialize Cloud Monitor if enabled
|
// Initialize Cloud Monitor if enabled
|
||||||
try {
|
try {
|
||||||
// Initialize DB & Migrate to Encrypted Storage
|
|
||||||
await CloudAccountRepo.init();
|
|
||||||
|
|
||||||
// Start OAuth Server
|
// Start OAuth Server
|
||||||
AuthServer.start();
|
AuthServer.start();
|
||||||
|
|
||||||
|
|||||||
+11
-1
@@ -17,9 +17,10 @@ import { getAppVersion, getPlatform } from '@/actions/app';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { setAppLanguage } from '@/actions/language';
|
import { setAppLanguage } from '@/actions/language';
|
||||||
import { useAppConfig } from '@/hooks/useAppConfig';
|
import { useAppConfig } from '@/hooks/useAppConfig';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2, FolderOpen } from 'lucide-react';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { ProxyConfig } from '@/types/config';
|
import { ProxyConfig } from '@/types/config';
|
||||||
|
import { openLogDirectory } from '@/actions/system';
|
||||||
|
|
||||||
function SettingsPage() {
|
function SettingsPage() {
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
@@ -193,6 +194,15 @@ function SettingsPage() {
|
|||||||
|
|
||||||
<div className="text-muted-foreground">{t('settings.license')}</div>
|
<div className="text-muted-foreground">{t('settings.license')}</div>
|
||||||
<div className="font-medium">CC BY-NC-SA 4.0</div>
|
<div className="font-medium">CC BY-NC-SA 4.0</div>
|
||||||
|
|
||||||
|
<div className="text-muted-foreground">{t('action.openLogs')}</div>
|
||||||
|
<button
|
||||||
|
onClick={() => openLogDirectory()}
|
||||||
|
className="flex items-center gap-2 font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||||
|
>
|
||||||
|
<FolderOpen className="h-4 w-4" />
|
||||||
|
<span>{t('settings.openLogDir', 'Open')}</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import { ProxyAgent } from 'undici';
|
|||||||
const CLIENT_ID = '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com';
|
const CLIENT_ID = '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com';
|
||||||
const CLIENT_SECRET = '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';
|
const CLIENT_SECRET = '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';
|
||||||
|
|
||||||
declare const fetch: any;
|
|
||||||
|
|
||||||
const URLS = {
|
const URLS = {
|
||||||
TOKEN: 'https://oauth2.googleapis.com/token',
|
TOKEN: 'https://oauth2.googleapis.com/token',
|
||||||
USER_INFO: 'https://www.googleapis.com/oauth2/v2/userinfo',
|
USER_INFO: 'https://www.googleapis.com/oauth2/v2/userinfo',
|
||||||
@@ -20,6 +18,18 @@ const URLS = {
|
|||||||
const USER_AGENT = 'antigravity/1.11.3 Darwin/arm64';
|
const USER_AGENT = 'antigravity/1.11.3 Darwin/arm64';
|
||||||
const REDIRECT_URI = 'http://localhost:8888/oauth-callback';
|
const REDIRECT_URI = 'http://localhost:8888/oauth-callback';
|
||||||
|
|
||||||
|
// Request timeout in milliseconds (30 seconds)
|
||||||
|
const REQUEST_TIMEOUT_MS = 30000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an AbortSignal that times out after the specified duration.
|
||||||
|
*/
|
||||||
|
function createTimeoutSignal(ms: number): AbortSignal {
|
||||||
|
const controller = new AbortController();
|
||||||
|
setTimeout(() => controller.abort(), ms);
|
||||||
|
return controller.signal;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Types ---
|
// --- Types ---
|
||||||
|
|
||||||
export interface TokenResponse {
|
export interface TokenResponse {
|
||||||
@@ -65,7 +75,7 @@ interface LoadProjectResponse {
|
|||||||
// --- Service Implementation ---
|
// --- Service Implementation ---
|
||||||
|
|
||||||
export class GoogleAPIService {
|
export class GoogleAPIService {
|
||||||
private static getFetchOptions(): any {
|
private static getFetchOptions() {
|
||||||
try {
|
try {
|
||||||
const config = ConfigManager.loadConfig();
|
const config = ConfigManager.loadConfig();
|
||||||
if (config.proxy?.upstream_proxy?.enabled && config.proxy.upstream_proxy.url) {
|
if (config.proxy?.upstream_proxy?.enabled && config.proxy.upstream_proxy.url) {
|
||||||
@@ -121,7 +131,17 @@ export class GoogleAPIService {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
body: params,
|
body: params,
|
||||||
|
signal: createTimeoutSignal(REQUEST_TIMEOUT_MS),
|
||||||
...this.getFetchOptions(),
|
...this.getFetchOptions(),
|
||||||
|
}).catch((err: unknown) => {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
if (err.name === 'AbortError') {
|
||||||
|
throw new Error(
|
||||||
|
'Token exchange timed out. Please check your network connection and try again.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -147,7 +167,17 @@ export class GoogleAPIService {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
body: params,
|
body: params,
|
||||||
|
signal: createTimeoutSignal(REQUEST_TIMEOUT_MS),
|
||||||
...this.getFetchOptions(),
|
...this.getFetchOptions(),
|
||||||
|
}).catch((err: unknown) => {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
if (err.name === 'AbortError') {
|
||||||
|
throw new Error(
|
||||||
|
'Token refresh timed out. Please check your network connection and try again.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -166,7 +196,17 @@ export class GoogleAPIService {
|
|||||||
static async getUserInfo(accessToken: string): Promise<UserInfo> {
|
static async getUserInfo(accessToken: string): Promise<UserInfo> {
|
||||||
const response = await fetch(URLS.USER_INFO, {
|
const response = await fetch(URLS.USER_INFO, {
|
||||||
headers: { Authorization: `Bearer ${accessToken}` },
|
headers: { Authorization: `Bearer ${accessToken}` },
|
||||||
|
signal: createTimeoutSignal(REQUEST_TIMEOUT_MS),
|
||||||
...this.getFetchOptions(),
|
...this.getFetchOptions(),
|
||||||
|
}).catch((err: unknown) => {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
if (err.name === 'AbortError') {
|
||||||
|
throw new Error(
|
||||||
|
'User info request timed out. Please check your network connection and try again.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -221,7 +261,7 @@ export class GoogleAPIService {
|
|||||||
const projectId = await this.fetchProjectId(accessToken);
|
const projectId = await this.fetchProjectId(accessToken);
|
||||||
|
|
||||||
// 2. Build Payload
|
// 2. Build Payload
|
||||||
const payload: Record<string, any> = {};
|
const payload: Record<string, unknown> = {};
|
||||||
if (projectId) {
|
if (projectId) {
|
||||||
payload['project'] = projectId;
|
payload['project'] = projectId;
|
||||||
}
|
}
|
||||||
@@ -285,11 +325,13 @@ export class GoogleAPIService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof Error) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[GoogleAPIService] Request failed: ${e.message} (Attempt ${attempt}/${maxRetries})`,
|
`[GoogleAPIService] Request failed: ${e.message} (Attempt ${attempt}/${maxRetries})`,
|
||||||
);
|
);
|
||||||
lastError = e;
|
}
|
||||||
|
lastError = e instanceof Error ? e : new Error(String(e));
|
||||||
if (attempt < maxRetries) {
|
if (attempt < maxRetries) {
|
||||||
await new Promise((r) => setTimeout(r, 1000));
|
await new Promise((r) => setTimeout(r, 1000));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
|
||||||
|
// Mock module to test timeout behavior
|
||||||
|
describe('GoogleAPIService Timeout', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should abort request after timeout', async () => {
|
||||||
|
const TIMEOUT_MS = 100;
|
||||||
|
|
||||||
|
// Recreate the same logic used in GoogleAPIService
|
||||||
|
const createTimeoutSignal = (ms: number): AbortSignal => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
setTimeout(() => controller.abort(), ms);
|
||||||
|
return controller.signal;
|
||||||
|
};
|
||||||
|
|
||||||
|
const signal = createTimeoutSignal(TIMEOUT_MS);
|
||||||
|
|
||||||
|
// Verify signal is not aborted initially
|
||||||
|
expect(signal.aborted).toBe(false);
|
||||||
|
|
||||||
|
// Fast-forward time
|
||||||
|
vi.advanceTimersByTime(TIMEOUT_MS + 10);
|
||||||
|
|
||||||
|
// Signal should now be aborted
|
||||||
|
expect(signal.aborted).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set aborted state when controller.abort() is called', () => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
// Initially not aborted
|
||||||
|
expect(controller.signal.aborted).toBe(false);
|
||||||
|
|
||||||
|
// Abort the controller
|
||||||
|
controller.abort();
|
||||||
|
|
||||||
|
// Signal should now be aborted
|
||||||
|
expect(controller.signal.aborted).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transform AbortError to user-friendly message', () => {
|
||||||
|
// This tests our error transformation logic from GoogleAPIService
|
||||||
|
const handleAbortError = (err: Error) => {
|
||||||
|
if (err.name === 'AbortError') {
|
||||||
|
throw new Error(
|
||||||
|
'Token exchange timed out. Please check your network connection and try again.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
};
|
||||||
|
|
||||||
|
const abortError = new Error('The operation was aborted');
|
||||||
|
abortError.name = 'AbortError';
|
||||||
|
|
||||||
|
expect(() => handleAbortError(abortError)).toThrow(
|
||||||
|
'Token exchange timed out. Please check your network connection and try again.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not transform non-AbortError', () => {
|
||||||
|
const handleAbortError = (err: Error) => {
|
||||||
|
if (err.name === 'AbortError') {
|
||||||
|
throw new Error(
|
||||||
|
'Token exchange timed out. Please check your network connection and try again.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
};
|
||||||
|
|
||||||
|
const networkError = new Error('Network failure');
|
||||||
|
networkError.name = 'NetworkError';
|
||||||
|
|
||||||
|
expect(() => handleAbortError(networkError)).toThrow('Network failure');
|
||||||
|
});
|
||||||
|
});
|
||||||
+167
-40
@@ -1,55 +1,182 @@
|
|||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { isProcessRunning, closeAntigravity, startAntigravity } from '../../ipc/process/handler';
|
|
||||||
import { exec } from 'child_process';
|
|
||||||
|
|
||||||
vi.mock('child_process', () => {
|
// Mock find-process module
|
||||||
return {
|
vi.mock('find-process', () => ({
|
||||||
exec: vi.fn(),
|
default: vi.fn(),
|
||||||
default: { exec: vi.fn() },
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mock promisify to return a function that returns a promise
|
|
||||||
vi.mock('util', () => ({
|
|
||||||
promisify: (fn: unknown) => fn,
|
|
||||||
default: { promisify: (fn: unknown) => fn },
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Mock logger to avoid console output during tests
|
||||||
|
vi.mock('../../utils/logger', () => ({
|
||||||
|
logger: {
|
||||||
|
debug: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock paths module to avoid child_process issues
|
||||||
|
vi.mock('../../utils/paths', () => ({
|
||||||
|
getAntigravityExecutablePath: vi.fn(() => '/path/to/antigravity'),
|
||||||
|
isWsl: vi.fn(() => false),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Import after mocks are set up
|
||||||
|
import { isProcessRunning, closeAntigravity, startAntigravity } from '../../ipc/process/handler';
|
||||||
|
import findProcess from 'find-process';
|
||||||
|
|
||||||
describe('Process Handler', () => {
|
describe('Process Handler', () => {
|
||||||
it('should check if process is running', async () => {
|
const mockFindProcess = findProcess as unknown as ReturnType<typeof vi.fn>;
|
||||||
// Mock exec to return stdout
|
|
||||||
const execMock = exec as unknown as ReturnType<typeof vi.fn>;
|
beforeEach(() => {
|
||||||
execMock.mockImplementation((cmd, cb) => {
|
vi.clearAllMocks();
|
||||||
if (cb) cb(null, { stdout: '12345' }, { stderr: '' });
|
|
||||||
return { stdout: '12345' };
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait, promisify wraps exec.
|
describe('isProcessRunning', () => {
|
||||||
// If I mock exec, promisify(exec) will use the mock.
|
it('should return true when Antigravity main process is found on macOS', async () => {
|
||||||
// But my mock implementation needs to match what promisify expects or I need to mock the promisified function.
|
Object.defineProperty(process, 'platform', { value: 'darwin', configurable: true });
|
||||||
// Since I mocked 'util', promisify returns the fn itself.
|
Object.defineProperty(process, 'pid', { value: 1000, configurable: true });
|
||||||
// So execAsync IS exec.
|
|
||||||
// But exec signature is (cmd, options, callback) or (cmd, callback).
|
|
||||||
// My implementation calls execAsync(command).
|
|
||||||
// So I should mock exec to return a Promise if I mocked promisify to return fn?
|
|
||||||
// No, if promisify returns fn, then `await execAsync(cmd)` means `await exec(cmd)`.
|
|
||||||
// `exec` returns a ChildProcess, not a Promise.
|
|
||||||
// So my mock of `util` is wrong if I want `await execAsync` to work.
|
|
||||||
// Better to mock the module that exports execAsync if possible, or mock `util` correctly.
|
|
||||||
// Or just mock `child_process` and use `vi.mocked(exec)`.
|
|
||||||
// But `promisify` is the key.
|
|
||||||
|
|
||||||
// Let's rely on vitest mocking capabilities.
|
mockFindProcess.mockResolvedValue([
|
||||||
// If I don't mock util, promisify will wrap the mocked exec.
|
{
|
||||||
// The mocked exec should behave like real exec (taking a callback).
|
pid: 12345,
|
||||||
|
name: 'Antigravity',
|
||||||
|
cmd: '/Applications/Antigravity.app/Contents/MacOS/Antigravity',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await isProcessRunning();
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(mockFindProcess).toHaveBeenCalledWith('name', 'Antigravity', true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Skip complex mocking tests for now as they are fragile without proper setup.
|
it('should return false when only helper processes are found', async () => {
|
||||||
// I'll write a basic test structure.
|
Object.defineProperty(process, 'platform', { value: 'darwin', configurable: true });
|
||||||
|
Object.defineProperty(process, 'pid', { value: 1000, configurable: true });
|
||||||
|
|
||||||
it('should be defined', () => {
|
mockFindProcess.mockResolvedValue([
|
||||||
|
{
|
||||||
|
pid: 12346,
|
||||||
|
name: 'Antigravity Helper (Renderer)',
|
||||||
|
cmd: '/Applications/Antigravity.app/Contents/Frameworks/Antigravity Helper (Renderer).app --type=renderer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pid: 12347,
|
||||||
|
name: 'Antigravity Helper (GPU)',
|
||||||
|
cmd: '/Applications/Antigravity.app/Contents/Frameworks/Antigravity Helper (GPU).app --type=gpu-process',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await isProcessRunning();
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when only manager process is found', async () => {
|
||||||
|
Object.defineProperty(process, 'platform', { value: 'darwin', configurable: true });
|
||||||
|
Object.defineProperty(process, 'pid', { value: 1000, configurable: true });
|
||||||
|
|
||||||
|
mockFindProcess.mockResolvedValue([
|
||||||
|
{
|
||||||
|
pid: 12348,
|
||||||
|
name: 'Antigravity Manager',
|
||||||
|
cmd: '/Applications/Antigravity Manager.app/Contents/MacOS/Antigravity Manager',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await isProcessRunning();
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when no processes are found', async () => {
|
||||||
|
Object.defineProperty(process, 'platform', { value: 'darwin', configurable: true });
|
||||||
|
Object.defineProperty(process, 'pid', { value: 1000, configurable: true });
|
||||||
|
|
||||||
|
mockFindProcess.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const result = await isProcessRunning();
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip self process', async () => {
|
||||||
|
Object.defineProperty(process, 'platform', { value: 'darwin', configurable: true });
|
||||||
|
Object.defineProperty(process, 'pid', { value: 12345, configurable: true });
|
||||||
|
|
||||||
|
mockFindProcess.mockResolvedValue([
|
||||||
|
{
|
||||||
|
pid: 12345, // Same as current PID
|
||||||
|
name: 'Antigravity',
|
||||||
|
cmd: '/Applications/Antigravity.app/Contents/MacOS/Antigravity',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await isProcessRunning();
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when Antigravity.exe is found on Windows', async () => {
|
||||||
|
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
||||||
|
Object.defineProperty(process, 'pid', { value: 1000, configurable: true });
|
||||||
|
|
||||||
|
mockFindProcess.mockResolvedValue([
|
||||||
|
{
|
||||||
|
pid: 12345,
|
||||||
|
name: 'Antigravity.exe',
|
||||||
|
cmd: 'C:\\Program Files\\Antigravity\\Antigravity.exe',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await isProcessRunning();
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when antigravity is found on Linux', async () => {
|
||||||
|
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
|
||||||
|
Object.defineProperty(process, 'pid', { value: 1000, configurable: true });
|
||||||
|
|
||||||
|
mockFindProcess.mockResolvedValue([
|
||||||
|
{
|
||||||
|
pid: 12345,
|
||||||
|
name: 'antigravity',
|
||||||
|
cmd: '/usr/bin/antigravity',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await isProcessRunning();
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle find-process errors gracefully', async () => {
|
||||||
|
Object.defineProperty(process, 'platform', { value: 'darwin', configurable: true });
|
||||||
|
Object.defineProperty(process, 'pid', { value: 1000, configurable: true });
|
||||||
|
|
||||||
|
mockFindProcess.mockRejectedValue(new Error('Process enumeration failed'));
|
||||||
|
|
||||||
|
const result = await isProcessRunning();
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exclude processes with --type= argument', async () => {
|
||||||
|
Object.defineProperty(process, 'platform', { value: 'darwin', configurable: true });
|
||||||
|
Object.defineProperty(process, 'pid', { value: 1000, configurable: true });
|
||||||
|
|
||||||
|
mockFindProcess.mockResolvedValue([
|
||||||
|
{
|
||||||
|
pid: 12345,
|
||||||
|
name: 'Antigravity',
|
||||||
|
cmd: '/Applications/Antigravity.app/Contents/MacOS/Antigravity --type=utility',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await isProcessRunning();
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Module exports', () => {
|
||||||
|
it('should export all required functions', () => {
|
||||||
expect(isProcessRunning).toBeDefined();
|
expect(isProcessRunning).toBeDefined();
|
||||||
expect(closeAntigravity).toBeDefined();
|
expect(closeAntigravity).toBeDefined();
|
||||||
expect(startAntigravity).toBeDefined();
|
expect(startAntigravity).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
@@ -124,6 +124,8 @@ export function getAntigravityDbPaths(): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (process.platform === 'linux') {
|
if (process.platform === 'linux') {
|
||||||
|
paths.push(path.join(appData, 'User', 'globalStorage', 'state.vscdb'));
|
||||||
|
paths.push(path.join(appData, 'User', 'state.vscdb'));
|
||||||
paths.push(path.join(appData, 'state.vscdb'));
|
paths.push(path.join(appData, 'state.vscdb'));
|
||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user