Initial Commit

This commit is contained in:
Draculabo committed 2025-12-23 01:38:53 +08:00
commit d0417339c1
158 files changed
+31368

No files matched your search

+74
View File
@@ -0,0 +1,74 @@
import fs from 'fs';
import path from 'path';
import { getAgentDir } from './paths';
export type LogLevel = 'info' | 'warn' | 'error' | 'debug';
class Logger {
private logFilePath: string;
constructor() {
const agentDir = getAgentDir();
if (!fs.existsSync(agentDir)) {
try {
fs.mkdirSync(agentDir, { recursive: true });
} catch (e) {
console.error('Failed to create agent directory for logs', e);
}
}
this.logFilePath = path.join(agentDir, 'app.log');
}
private formatMessage(level: LogLevel, message: string, ...args: unknown[]): string {
const timestamp = new Date().toISOString();
const formattedArgs = args
.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
.join(' ');
return `[${timestamp}] [${level.toUpperCase()}] ${message} ${formattedArgs}`;
}
private writeToFile(formattedMessage: string) {
try {
fs.appendFileSync(this.logFilePath, formattedMessage + '\n');
} catch (e) {
console.error('Failed to write to log file', e);
}
}
private writeToConsole(level: LogLevel, message: string, ...args: unknown[]) {
const colorMap: Record<LogLevel, string> = {
info: '\x1b[36m', // Cyan
warn: '\x1b[33m', // Yellow
error: '\x1b[31m', // Red
debug: '\x1b[90m', // Gray
};
const reset = '\x1b[0m';
const color = colorMap[level];
console.log(`${color}[${level.toUpperCase()}]${reset} ${message}`, ...args);
}
log(level: LogLevel, message: string, ...args: unknown[]) {
const formattedMessage = this.formatMessage(level, message, ...args);
this.writeToConsole(level, message, ...args);
this.writeToFile(formattedMessage);
}
info(message: string, ...args: unknown[]) {
this.log('info', message, ...args);
}
warn(message: string, ...args: unknown[]) {
this.log('warn', message, ...args);
}
error(message: string, ...args: unknown[]) {
this.log('error', message, ...args);
}
debug(message: string, ...args: unknown[]) {
this.log('debug', message, ...args);
}
}
export const logger = new Logger();
+199
View File
@@ -0,0 +1,199 @@
import path from 'path';
import os from 'os';
import fs from 'fs';
import { execSync } from 'child_process';
/**
* Checks if the current platform is WSL.
* @returns {boolean} True if the current platform is WSL, false otherwise.
*/
export function isWsl(): boolean {
if (process.platform !== 'linux') return false;
try {
const version = fs.readFileSync('/proc/version', 'utf-8').toLowerCase();
return version.includes('microsoft') && version.includes('wsl');
} catch {
return false;
}
}
let cachedWindowsUser: string | null = null;
/**
* Gets the Windows username.
* @returns {string} The Windows username.
*/
function getWindowsUser(): string {
if (cachedWindowsUser) return cachedWindowsUser;
// Strategy 1: Try cmd.exe to get actual Windows username (most reliable for WSL)
try {
// We use execSync because this function needs to be synchronous
// and it's usually called once or cached.
const stdout = execSync('/mnt/c/Windows/System32/cmd.exe /c "echo %USERNAME%"', {
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'ignore'],
});
// Output might contain warnings about UNC paths, so we take the last line
const lines = stdout.trim().split(/\r?\n/);
const user = lines[lines.length - 1].trim();
if (user) {
cachedWindowsUser = user;
return user;
}
} catch {
// Ignore errors
}
// Strategy 2: Try to match current Linux username
const linuxUser = os.userInfo().username;
if (fs.existsSync(`/mnt/c/Users/${linuxUser}`)) {
cachedWindowsUser = linuxUser;
return linuxUser;
}
// Strategy 3: List users and pick first likely candidate
try {
const users = fs
.readdirSync('/mnt/c/Users')
.filter(
(u) =>
!['Public', 'Default', 'Default User', 'All Users', 'desktop.ini'].includes(u) &&
fs.statSync(path.join('/mnt/c/Users', u)).isDirectory(),
);
if (users.length > 0) {
cachedWindowsUser = users[0];
return users[0];
}
} catch {
// Ignore errors when reading directory
}
return 'User'; // Fallback
}
export function getAppDataDir(): string {
const home = os.homedir();
if (isWsl()) {
const winUser = getWindowsUser();
return `/mnt/c/Users/${winUser}/AppData/Roaming/Antigravity`;
}
switch (process.platform) {
case 'darwin':
return path.join(home, 'Library', 'Application Support', 'Antigravity');
case 'win32':
return path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Antigravity');
case 'linux':
return path.join(home, '.config', 'Antigravity');
default:
return path.join(home, '.antigravity');
}
}
export function getAgentDir(): string {
return path.join(os.homedir(), '.antigravity-agent');
}
export function getAccountsFilePath(): string {
return path.join(getAgentDir(), 'antigravity_accounts.json');
}
export function getBackupsDir(): string {
return path.join(getAgentDir(), 'backups');
}
export function getCloudAccountsDbPath(): string {
return path.join(getAgentDir(), 'cloud_accounts.db');
}
export function getAntigravityDbPaths(): string[] {
const appData = getAppDataDir();
const paths: string[] = [];
const home = os.homedir();
if (isWsl()) {
// Assume standard structure: AppData/Roaming/Antigravity/User/globalStorage/state.vscdb
// appData is already resolved to Roaming/Antigravity in getAppDataDir()
paths.push(path.join(appData, 'User', 'globalStorage', 'state.vscdb'));
paths.push(path.join(appData, 'User', 'state.vscdb'));
paths.push(path.join(appData, 'state.vscdb'));
return paths;
}
if (process.platform === 'linux') {
paths.push(path.join(appData, 'state.vscdb'));
return paths;
}
if (process.platform === 'darwin') {
// Standard path
paths.push(
path.join(
home,
'Library',
'Application Support',
'Antigravity',
'User',
'globalStorage',
'state.vscdb',
),
);
// Fallback path
paths.push(path.join(home, 'Library', 'Application Support', 'Antigravity', 'state.vscdb'));
return paths;
}
// Windows
// Standard path
paths.push(path.join(appData, 'User', 'globalStorage', 'state.vscdb'));
// Fallback paths
paths.push(path.join(appData, 'User', 'state.vscdb'));
paths.push(path.join(appData, 'state.vscdb'));
return paths;
}
// Keep for backward compatibility if needed, but prefer getAntigravityDbPaths
export function getAntigravityDbPath(): string {
const paths = getAntigravityDbPaths();
return paths.length > 0 ? paths[0] : '';
}
export function getAntigravityExecutablePath(): string {
if (isWsl()) {
const winUser = getWindowsUser();
return `/mnt/c/Users/${winUser}/AppData/Local/Programs/Antigravity/Antigravity.exe`;
}
switch (process.platform) {
case 'darwin':
return '/Applications/Antigravity.app/Contents/MacOS/Antigravity';
case 'win32': {
const localAppData = process.env.LOCALAPPDATA || '';
const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
const programFilesX86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
const possiblePaths = [
path.join(localAppData, 'Programs', 'Antigravity', 'Antigravity.exe'),
path.join(programFiles, 'Antigravity', 'Antigravity.exe'),
path.join(programFilesX86, 'Antigravity', 'Antigravity.exe'),
];
for (const p of possiblePaths) {
if (fs.existsSync(p)) {
return p;
}
}
// Fallback to default
return possiblePaths[0];
}
case 'linux':
return '/usr/share/antigravity/antigravity';
default:
return '';
}
}
+199
View File
@@ -0,0 +1,199 @@
export class ProtobufUtils {
static encodeVarint(value: number): Uint8Array {
// Note: Javascript bitwise operators treat operands as 32-bit integers.
// However, for typical token lengths and small field IDs, this is fine.
// Expiry timestamp (seconds) fits in 32-bit (until 2038) but might exceed if it's milliseconds.
// If value is larger than 2^32, we need BigInt.
const buf: number[] = [];
let val = BigInt(value);
while (val >= 128n) {
buf.push(Number((val & 127n) | 128n));
val >>= 7n;
}
buf.push(Number(val));
return new Uint8Array(buf);
}
static readVarint(data: Uint8Array, offset: number): { value: bigint; nextOffset: number } {
let result = 0n;
let shift = 0n;
let pos = offset;
while (pos < data.length) {
const byte = BigInt(data[pos]);
result |= (byte & 127n) << shift;
pos++;
if ((byte & 128n) === 0n) {
return { value: result, nextOffset: pos };
}
shift += 7n;
}
throw new Error('Incomplete varint data');
}
// Skip a field based on wire type
static skipField(data: Uint8Array, offset: number, wireType: number): number {
switch (wireType) {
case 0: // Varint
return this.readVarint(data, offset).nextOffset;
case 1: // 64-bit
return offset + 8;
case 2: // Length-delimited
const { value: length, nextOffset } = this.readVarint(data, offset);
return nextOffset + Number(length);
case 5: // 32-bit
return offset + 4;
default:
throw new Error(`Unknown wire type: ${wireType}`);
}
}
static removeField(data: Uint8Array, fieldNum: number): Uint8Array {
const result: number[] = [];
let offset = 0;
while (offset < data.length) {
const startOffset = offset;
const { value: tag, nextOffset } = this.readVarint(data, offset);
const wireType = Number(tag & 7n);
const currentField = Number(tag >> 3n);
if (currentField === fieldNum) {
// Skip
offset = this.skipField(data, nextOffset, wireType);
} else {
// Copy
const endOffset = this.skipField(data, nextOffset, wireType);
for (let i = startOffset; i < endOffset; i++) {
result.push(data[i]);
}
offset = endOffset;
}
}
return new Uint8Array(result);
}
// Create string field (WireType 2)
static createStringField(fieldNum: number, value: string): Uint8Array {
const tag = (fieldNum << 3) | 2;
const utf8Encode = new TextEncoder();
const bytes = utf8Encode.encode(value);
const tagBytes = this.encodeVarint(tag);
const lenBytes = this.encodeVarint(bytes.length);
const result = new Uint8Array(tagBytes.length + lenBytes.length + bytes.length);
result.set(tagBytes, 0);
result.set(lenBytes, tagBytes.length);
result.set(bytes, tagBytes.length + lenBytes.length);
return result;
}
// Create timestamp field (Field 4 -> Field 1 varint)
static createTimestampField(fieldNum: number, seconds: number): Uint8Array {
// Timestamp message format: Field 1 (seconds) as varint
const innerTag = (1 << 3) | 0;
const innerTagBytes = this.encodeVarint(innerTag);
const secondsBytes = this.encodeVarint(seconds);
const innerMsg = new Uint8Array(innerTagBytes.length + secondsBytes.length);
innerMsg.set(innerTagBytes, 0);
innerMsg.set(secondsBytes, innerTagBytes.length);
// Wrap in length delimited
const tag = (fieldNum << 3) | 2;
const tagBytes = this.encodeVarint(tag);
const lenBytes = this.encodeVarint(innerMsg.length);
const result = new Uint8Array(tagBytes.length + lenBytes.length + innerMsg.length);
result.set(tagBytes, 0);
result.set(lenBytes, tagBytes.length);
result.set(innerMsg, tagBytes.length + lenBytes.length);
return result;
}
// Extract field by ID
static getField(data: Uint8Array, fieldNum: number): Uint8Array | null {
let offset = 0;
while (offset < data.length) {
const { value: tag, nextOffset } = this.readVarint(data, offset);
const wireType = Number(tag & 7n);
const currentField = Number(tag >> 3n);
if (currentField === fieldNum) {
if (wireType === 2) {
// Length delimited
const { value: length, nextOffset: dataStart } = this.readVarint(data, nextOffset);
return data.slice(dataStart, dataStart + Number(length));
}
// For sync feature, we mainly care about wireType 2 (string/bytes)
return null;
}
offset = this.skipField(data, nextOffset, wireType);
}
return null;
}
static readString(data: Uint8Array): string {
const dec = new TextDecoder();
return dec.decode(data);
}
static extractOAuthTokenInfo(
data: Uint8Array,
): { accessToken: string; refreshToken: string } | null {
// 1. Find Field 6 (OAuthTokenInfo)
const field6Data = this.getField(data, 6);
if (!field6Data) return null;
// 2. Parse Field 6 content
// Field 1: Access Token (String)
// Field 2: Type (String, "Bearer")
// Field 3: Refresh Token (String)
// Field 4: Expiry (Varint? Timestamp?) - In create we used createTimestampField which effectively makes it Field 4 -> Field 1.
// But here we just need Access/Refresh.
const accessTokenBytes = this.getField(field6Data, 1);
const refreshTokenBytes = this.getField(field6Data, 3);
if (accessTokenBytes && refreshTokenBytes) {
return {
accessToken: this.readString(accessTokenBytes),
refreshToken: this.readString(refreshTokenBytes),
};
}
return null;
}
static createOAuthTokenInfo(
accessToken: string,
refreshToken: string,
expiry: number,
): Uint8Array {
const f1 = this.createStringField(1, accessToken);
const f2 = this.createStringField(2, 'Bearer');
const f3 = this.createStringField(3, refreshToken);
const f4 = this.createTimestampField(4, expiry);
const combined = new Uint8Array(f1.length + f2.length + f3.length + f4.length);
combined.set(f1, 0);
combined.set(f2, f1.length);
combined.set(f3, f1.length + f2.length);
combined.set(f4, f1.length + f2.length + f3.length);
// Wrap as Field 6
const tag6 = (6 << 3) | 2;
const tag6Bytes = this.encodeVarint(tag6);
const lenBytes = this.encodeVarint(combined.length);
const result = new Uint8Array(tag6Bytes.length + lenBytes.length + combined.length);
result.set(tag6Bytes, 0);
result.set(lenBytes, tag6Bytes.length);
result.set(combined, tag6Bytes.length + lenBytes.length);
return result;
}
}
+16
View File
@@ -0,0 +1,16 @@
import { routeTree } from "@/routeTree.gen";
import { createMemoryHistory, createRouter } from "@tanstack/react-router";
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
export const router = createRouter({
defaultPendingMinMs: 0,
routeTree,
history: createMemoryHistory({
initialEntries: ["/"],
}),
});
+100
View File
@@ -0,0 +1,100 @@
import keytar from 'keytar';
import crypto from 'crypto';
import { logger } from './logger';
const SERVICE_NAME = 'AntigravityManager';
const ACCOUNT_NAME = 'MasterKey';
// Cache the key in memory to avoid frequent system calls
let cachedMasterKey: Buffer | null = null;
async function getOrGenerateMasterKey(): Promise<Buffer> {
if (cachedMasterKey) return cachedMasterKey;
try {
let hexKey = await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
if (!hexKey) {
logger.info('Security: Generating new master key...');
// Generate 256-bit key (32 bytes)
const buffer = crypto.randomBytes(32);
hexKey = buffer.toString('hex');
await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, hexKey);
}
cachedMasterKey = Buffer.from(hexKey, 'hex');
return cachedMasterKey;
} catch (error) {
logger.error('Security: Failed to access keychain/credential manager', error);
// Fallback? If we can't store the key, we can't persistently encrypt.
// For now, throw to prevent data loss (better not to write than to write something we can't decrypt later or write plain text when promised encrypted)
throw new Error('Key management system unavailable');
}
}
const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 16;
const AUTH_TAG_LENGTH = 16;
/**
* Encrypts a string using AES-256-GCM.
* Output format: "iv_hex:auth_tag_hex:ciphertext_hex"
*/
export async function encrypt(text: string): Promise<string> {
try {
const key = await getOrGenerateMasterKey();
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
// Format: iv:authTag:encrypted
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
} catch (error) {
logger.error('Security: Encryption failed', error);
throw error;
}
}
/**
* Decrypts a string using AES-256-GCM.
* Input format: "iv_hex:auth_tag_hex:ciphertext_hex"
*/
export async function decrypt(text: string): Promise<string> {
// Check if it's plain text (JSON) for backward compatibility
// Very rough check: starts with { or [
if (text.startsWith('{') || text.startsWith('[')) {
return text;
}
// Also checking if it follows our pattern
const parts = text.split(':');
if (parts.length !== 3) {
// Treat as plain text if it doesn't look like our encrypted format
return text;
}
try {
const key = await getOrGenerateMasterKey();
const [ivHex, authTagHex, encryptedHex] = parts;
const iv = Buffer.from(ivHex, 'hex');
const authTag = Buffer.from(authTagHex, 'hex');
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encryptedHex, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
} catch (error) {
logger.error('Security: Decryption failed', error);
// If decryption fails, it might be corrupted or using a different key.
throw error;
}
}