Compare commits

..

No commits in common. "main" and "v1.0.0" have entirely different histories.
main ... v1.0.0

61 changed files with 7803 additions and 9394 deletions

57
.gitignore vendored Normal file → Executable file
View file

@ -1,30 +1,27 @@
# Logs # Logs
logs logs
*.log *.log
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
pnpm-debug.log* pnpm-debug.log*
lerna-debug.log* lerna-debug.log*
node_modules node_modules
dist dist
dist-ssr dist-ssr
*.local *.local
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*
!.vscode/extensions.json !.vscode/extensions.json
.idea .idea
.DS_Store .DS_Store
*.suo *.suo
*.ntvs* *.ntvs*
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
# Release Artifacts
Release/ # Release artifacts
release/ release/
*.zip
*.exe
backend/dist/

0
.npmrc Normal file → Executable file
View file

135
README.md Normal file → Executable file
View file

@ -1,67 +1,68 @@
# KV Clearnup (Antigravity) 🚀 # KV Clearnup (Antigravity) 🚀
A modern, high-performance system optimizer for macOS, built with **Electron**, **React**, and **Go**. A modern, high-performance system optimizer for macOS, built with **Electron**, **React**, and **Go**.
![App Screenshot](https://via.placeholder.com/800x500?text=Antigravity+Dashboard) ![App Screenshot](https://via.placeholder.com/800x500?text=Antigravity+Dashboard)
## Features ## Features
- **Flash Clean**: Instantly remove system caches, logs, Xcode cache, Homebrew cache, and manage Trash with a detailed inspection view. - **Flash Clean**: Instantly remove system caches, logs, and trash.
- **App Uninstaller**: View installed applications, their sizes, and thoroughly remove them along with their associated preference files and caches. - **Deep Clean**: Scan for large files and heavy folders.
- **Deep Clean**: Scan for large files and heavy folders. - **Real-time Monitoring**: Track disk usage and category sizes.
- **Real-time Monitoring**: Track disk usage and category sizes. - **Universal Binary**: Runs natively on both Apple Silicon (M1/M2/M3) and Intel Macs.
- **Native Menubar Integration**: Includes a responsive, monochrome template icon that adapts to macOS light/dark modes perfectly. - **High Performance**: Heavy lifting is handled by a compiled Go backend.
- **Cross-Platform**: Runs natively with compiled Go backends on Apple Silicon (M1/M2/M3), Intel Macs, and Windows.
## Prerequisites
## Prerequisites - **Node.js** (v18+)
- **Node.js** (v18+) - **Go** (v1.20+)
- **Go** (v1.20+) - **pnpm** (preferred) or npm
- **pnpm** (preferred) or npm
- **C Compiler** (gcc/clang, via Xcode Command Line Tools on macOS) ## Development
## Development ### 1. Install Dependencies
```bash
### 1. Install Dependencies npm install
```bash ```
pnpm install
``` ### 2. Run in Development Mode
This starts the Go backend (port 36969) and the Vite/Electron frontend concurrently.
### 2. Run in Development Mode ```bash
This starts the Go backend (port 36969) and the Vite/Electron frontend concurrently. ./start-go.sh
```bash ```
./start-go.sh *Note: Do not run `npm run dev` directly if you want the backend to work. Use the script.*
```
*Note: Do not run `pnpm run dev` directly if you want the backend to work. Use the script.* ## Building for Production
## Building for Production To create a distributable `.dmg` file for macOS:
To create distributable release binaries (Universal `.dmg` for macOS, Portable `.exe` for Windows): ### 1. Build the App
```bash
### 1. Build the App npm run build:mac
```bash ```
# macOS Universal DMG This command will:
pnpm run build && pnpm run electron:build && npx electron-builder --mac --universal 1. Compile the Go backend for both `amd64` and `arm64`.
2. Create a universal binary using `lipo`.
# Windows Portable EXE 3. Build the React frontend.
pnpm run build && pnpm run electron:build && npx electron-builder --win portable --x64 4. Package the Electron app and bundle the backend.
``` 5. Generate a universal `.dmg`.
### 2. Locate the Installer ### 2. Locate the Installer
The output files will be automatically placed in the `release/` directory: The output file will be at:
- `release/KV Clearnup-1.0.0-universal.dmg` (macOS) ```
- `release/KV Clearnup 1.0.0.exe` (Windows) release/KV Clearnup-1.0.0-universal.dmg
```
## Running the App
1. **Mount the DMG**: Double-click the `.dmg` file in the `release` folder. ## Running the App
2. **Install**: Drag the app to your `Applications` folder. 1. **Mount the DMG**: Double-click the `.dmg` file in the `release` folder.
3. **Launch**: Open "KV Clearnup" from Applications. 2. **Install**: Drag the app to your `Applications` folder.
3. **Launch**: Open "KV Clearnup" from Applications.
*Troubleshooting*: If you see "System Extension Blocked" or similar OS warnings, go to **System Settings > Privacy & Security** and allow the application.
*Troubleshooting*: If you see "System Extension Blocked" or similar OS warnings, go to **System Settings > Privacy & Security** and allow the application.
## Architecture
- **Frontend**: React, TypeScript, TailwindCSS, Framer Motion. ## Architecture
- **Main Process**: Electron (TypeScript). - **Frontend**: React, TypeScript, TailwindCSS, Framer Motion.
- **Backend**: Go (Golang) for file system operations and heavy scanning. - **Main Process**: Electron (TypeScript).
- **Communication**: Electron uses `child_process` to spawn the Go binary. Frontend communicates with backend via HTTP (localhost:36969). - **Backend**: Go (Golang) for file system operations and heavy scanning.
- **Communication**: Electron uses `child_process` to spawn the Go binary. Frontend communicates with backend via HTTP (localhost:36969).
## License
MIT ## License
MIT

View file

@ -1,5 +1,3 @@
//go:build darwin
package apps package apps
import ( import (
@ -11,7 +9,25 @@ import (
"sync" "sync"
) )
// Structs moved to apps_common.go type AppInfo struct {
Name string `json:"name"`
Path string `json:"path"`
BundleID string `json:"bundleID"`
Size int64 `json:"size"`
Icon string `json:"icon,omitempty"` // Base64 or path? For now just path to .app (frontend can get icon)
}
type AssociatedFile struct {
Path string `json:"path"`
Type string `json:"type"` // "cache", "config", "log", "data"
Size int64 `json:"size"`
}
type AppDetails struct {
AppInfo
Associated []AssociatedFile `json:"associated"`
TotalSize int64 `json:"totalSize"`
}
// ScanApps returns a list of installed applications // ScanApps returns a list of installed applications
func ScanApps() ([]AppInfo, error) { func ScanApps() ([]AppInfo, error) {
@ -65,7 +81,7 @@ func ScanApps() ([]AppInfo, error) {
} }
// GetAppDetails finds all associated files for a given app path // GetAppDetails finds all associated files for a given app path
func GetAppDetails(appPath, _ string) (*AppDetails, error) { func GetAppDetails(appPath string) (*AppDetails, error) {
bid := getBundleID(appPath) bid := getBundleID(appPath)
if bid == "" { if bid == "" {
return nil, fmt.Errorf("could not determine bundle ID") return nil, fmt.Errorf("could not determine bundle ID")
@ -199,8 +215,3 @@ func getType(locName string) string {
return "data" return "data"
} }
} }
// RunUninstaller executes the uninstall command (Not implemented on Mac yet)
func RunUninstaller(cmdString string) error {
return fmt.Errorf("uninstall not supported on macOS yet")
}

View file

@ -1,22 +0,0 @@
package apps
type AppInfo struct {
Name string `json:"name"`
Path string `json:"path"`
BundleID string `json:"bundleID"` // On Windows this can be ProductCode or Registry Key Name
UninstallString string `json:"uninstallString"`
Size int64 `json:"size"`
Icon string `json:"icon,omitempty"`
}
type AssociatedFile struct {
Path string `json:"path"`
Type string `json:"type"` // "cache", "config", "log", "data"
Size int64 `json:"size"`
}
type AppDetails struct {
AppInfo
Associated []AssociatedFile `json:"associated"`
TotalSize int64 `json:"totalSize"`
}

View file

@ -1,248 +0,0 @@
//go:build windows
package apps
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"golang.org/x/sys/windows/registry"
)
// ScanApps returns a list of installed applications via Registry
func ScanApps() ([]AppInfo, error) {
var apps []AppInfo
// Keys to search
// HKLM Software\Microsoft\Windows\CurrentVersion\Uninstall
// HKLM Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall
// HKCU Software\Microsoft\Windows\CurrentVersion\Uninstall
keys := []struct {
hive registry.Key
path string
}{
{registry.LOCAL_MACHINE, `Software\Microsoft\Windows\CurrentVersion\Uninstall`},
{registry.LOCAL_MACHINE, `Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall`},
{registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Uninstall`},
}
seen := make(map[string]bool)
for _, k := range keys {
baseKey, err := registry.OpenKey(k.hive, k.path, registry.READ)
if err != nil {
continue
}
subkeys, err := baseKey.ReadSubKeyNames(-1)
baseKey.Close()
if err != nil {
continue
}
for _, subkeyName := range subkeys {
appKey, err := registry.OpenKey(k.hive, k.path+`\`+subkeyName, registry.READ)
if err != nil {
continue
}
displayName, _, err := appKey.GetStringValue("DisplayName")
if err != nil || displayName == "" {
appKey.Close()
continue
}
// Define installLocation explicitly
installLocation, _, _ := appKey.GetStringValue("InstallLocation")
uninstallString, _, _ := appKey.GetStringValue("UninstallString")
quietUninstallString, _, _ := appKey.GetStringValue("QuietUninstallString")
if uninstallString == "" && quietUninstallString != "" {
uninstallString = quietUninstallString
}
// Debug Log
if strings.Contains(displayName, "Foxit") {
fmt.Printf("found Foxit: %s | UninstallString: %s\n", displayName, uninstallString)
}
// Deduplication: If we've seen this Name + Location combination, skip it.
// This handles the common case of 32-bit apps appearing in both HKLM and WOW6432Node.
dedupKey := displayName + "|" + strings.ToLower(installLocation)
if seen[dedupKey] {
appKey.Close()
continue
}
seen[dedupKey] = true
// Try to get size from registry (EstimatedSize is in KB)
sizeVal, _, errSize := appKey.GetIntegerValue("EstimatedSize")
var sizeBytes int64
if errSize == nil {
sizeBytes = int64(sizeVal) * 1024
}
// Construct Full Registry Key Path as BundleID for later use
hiveName := "HKLM"
if k.hive == registry.CURRENT_USER {
hiveName = "HKCU"
}
fullRegPath := hiveName + `\` + k.path + `\` + subkeyName
apps = append(apps, AppInfo{
Name: displayName,
Path: installLocation,
BundleID: fullRegPath,
UninstallString: uninstallString,
Size: sizeBytes,
})
appKey.Close()
}
}
return apps, nil
}
// GetAppDetails finds all associated files (simplified for Windows)
func GetAppDetails(appPath, bundleID string) (*AppDetails, error) {
// appPath might come from ScanApps which set it to InstallLocation.
// bundleID is used as the Registry Key Path.
// Re-construct basic info
info := AppInfo{
Name: filepath.Base(appPath),
Path: appPath,
BundleID: bundleID,
// UninstallString is hard to recover if not passed, but usually we call GetAppDetails after ScanApps which has it.
// For now leave empty, or we'd need to re-query registry if bundleID is a registry path.
Size: 0,
}
if appPath == "" && bundleID != "" {
// Fallback name if path is empty
parts := strings.Split(bundleID, `\`)
if len(parts) > 0 {
info.Name = parts[len(parts)-1]
}
}
details := &AppDetails{
AppInfo: info,
TotalSize: 0,
}
// 1. Scan File System
if appPath != "" {
var size int64
filepath.WalkDir(appPath, func(_ string, d os.DirEntry, err error) error {
if err == nil && !d.IsDir() {
i, _ := d.Info()
size += i.Size()
}
return nil
})
details.AppInfo.Size = size
details.TotalSize = size
// Add the main folder as associated data
details.Associated = append(details.Associated, AssociatedFile{
Path: appPath,
Type: "data",
Size: size,
})
}
// 2. Add Registry Key (Uninstall Entry)
if bundleID != "" && (strings.HasPrefix(bundleID, "HKLM") || strings.HasPrefix(bundleID, "HKCU")) {
// We treat the registry key as a "file" with special type and 0 size
details.Associated = append(details.Associated, AssociatedFile{
Path: "REG:" + bundleID,
Type: "registry", // New type
Size: 0, // Registry entries are negligible in size
})
}
return details, nil
}
// DeleteFiles removes the requested paths
func DeleteFiles(paths []string) error {
for _, p := range paths {
if p == "" {
continue
}
// Registry Deletion
if strings.HasPrefix(p, "REG:") {
regPath := strings.TrimPrefix(p, "REG:")
deleteRegistryKey(regPath)
continue
}
// Safety checks
if p == "C:\\" || p == "c:\\" ||
p == "C:\\Windows" || strings.HasPrefix(strings.ToLower(p), "c:\\windows") {
continue
}
err := os.RemoveAll(p)
if err != nil {
// Log error but continue? Or return?
// return err
// On Windows file locking is common, best effort
}
}
return nil
}
func deleteRegistryKey(fullPath string) error {
var hive registry.Key
var subPath string
if strings.HasPrefix(fullPath, "HKLM\\") {
hive = registry.LOCAL_MACHINE
subPath = strings.TrimPrefix(fullPath, "HKLM\\")
} else if strings.HasPrefix(fullPath, "HKCU\\") {
hive = registry.CURRENT_USER
subPath = strings.TrimPrefix(fullPath, "HKCU\\")
} else {
return nil
}
// Provide parent key and subkey name to DeleteKey
// path: Software\...\Uninstall\AppGUID
lastSlash := strings.LastIndex(subPath, `\`)
if lastSlash == -1 {
return nil
}
parentPath := subPath[:lastSlash]
keyName := subPath[lastSlash+1:]
k, err := registry.OpenKey(hive, parentPath, registry.WRITE)
if err != nil {
return err
}
defer k.Close()
return registry.DeleteKey(k, keyName)
}
// RunUninstaller executes the uninstall command
func RunUninstaller(cmdString string) error {
fmt.Printf("RunUninstaller Called with: %s\n", cmdString)
cmd := exec.Command("cmd", "/C", cmdString)
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: false} // Show window so user can click next
err := cmd.Start()
if err != nil {
fmt.Printf("RunUninstaller Error: %v\n", err)
return err
}
fmt.Printf("RunUninstaller Started Successfully\n")
return nil
}

View file

@ -1,8 +0,0 @@
package platform
type SystemInfo struct {
Model string `json:"model"`
Chip string `json:"chip"`
Memory string `json:"memory"`
OS string `json:"os"`
}

View file

@ -1,114 +0,0 @@
//go:build darwin
package platform
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
)
func OpenSettings() error {
return exec.Command("open", "x-apple.systempreferences:com.apple.settings.Storage").Run()
}
func GetSystemInfo() (*SystemInfo, error) {
// Structs for parsing system_profiler JSON
type HardwareItem struct {
MachineName string `json:"machine_name"`
ChipType string `json:"chip_type"`
PhysicalMemory string `json:"physical_memory"`
}
type SoftwareItem struct {
OSVersion string `json:"os_version"`
}
type SystemProfile struct {
Hardware []HardwareItem `json:"SPHardwareDataType"`
Software []SoftwareItem `json:"SPSoftwareDataType"`
}
cmd := exec.Command("system_profiler", "SPHardwareDataType", "SPSoftwareDataType", "-json")
output, err := cmd.Output()
if err != nil {
return nil, err
}
var profile SystemProfile
if err := json.Unmarshal(output, &profile); err != nil {
return nil, err
}
info := &SystemInfo{
Model: "Unknown",
Chip: "Unknown",
Memory: "Unknown",
OS: "Unknown",
}
if len(profile.Hardware) > 0 {
info.Model = profile.Hardware[0].MachineName
info.Chip = profile.Hardware[0].ChipType
info.Memory = profile.Hardware[0].PhysicalMemory
}
if len(profile.Software) > 0 {
info.OS = profile.Software[0].OSVersion
}
return info, nil
}
func EmptyTrash() error {
home, err := os.UserHomeDir()
if err != nil {
return err
}
trashPath := filepath.Join(home, ".Trash")
entries, err := os.ReadDir(trashPath)
if err != nil {
return err
}
for _, entry := range entries {
itemPath := filepath.Join(trashPath, entry.Name())
os.RemoveAll(itemPath)
}
return nil
}
func GetCachePath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, "Library", "Caches"), nil
}
func GetDockerPath() (string, error) {
dockerPath, err := exec.LookPath("docker")
if err != nil {
// Try common locations
commonPaths := []string{
"/usr/local/bin/docker",
"/opt/homebrew/bin/docker",
"/Applications/Docker.app/Contents/Resources/bin/docker",
}
for _, p := range commonPaths {
if _, e := os.Stat(p); e == nil {
dockerPath = p
return dockerPath, nil
}
}
}
if dockerPath != "" {
return dockerPath, nil
}
return "", fmt.Errorf("docker not found")
}
func OpenBrowser(url string) error {
return exec.Command("open", url).Start()
}

View file

@ -1,106 +0,0 @@
//go:build windows
package platform
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
func OpenSettings() error {
// Open Windows Settings -> Storage
// ms-settings:storagesense
return exec.Command("cmd", "/c", "start", "ms-settings:storagesense").Run()
}
func GetSystemInfo() (*SystemInfo, error) {
// Use systeminfo or wmic
// simpler: generic info
info := &SystemInfo{
Model: "PC",
Chip: "Unknown",
Memory: "Unknown",
OS: "Windows",
}
// Helper to run powershell and get string result
runPS := func(cmd string) string {
out, err := exec.Command("powershell", "-NoProfile", "-Command", cmd).Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}
// 1. Get OS Name (Simplified)
// Get-CimInstance Win32_OperatingSystem | Select-Object -ExpandProperty Caption
osName := runPS("(Get-CimInstance Win32_OperatingSystem).Caption")
if osName != "" {
info.OS = strings.TrimPrefix(osName, "Microsoft ")
}
// 2. Get Memory (in GB)
// [math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB)
mem := runPS("[math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB)")
if mem != "" {
info.Memory = mem + " GB"
}
// 3. Get CPU Name
// (Get-CimInstance Win32_Processor).Name
cpu := runPS("(Get-CimInstance Win32_Processor).Name")
if cpu != "" {
// Cleanup CPU string (remove extra spaces)
info.Chip = strings.Join(strings.Fields(cpu), " ")
}
return info, nil
}
func EmptyTrash() error {
// PowerShell to empty Recycle Bin
// Clear-RecycleBin -Force -ErrorAction SilentlyContinue
// PowerShell to empty Recycle Bin
// Clear-RecycleBin -Force -ErrorAction SilentlyContinue
// We use ExecutionPolicy Bypass to avoid permission issues.
// We also catch errors to prevent 500s on empty bins.
cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", "Clear-RecycleBin -Force -ErrorAction SilentlyContinue")
// If it returns an error, it might be due to permissions or being already empty.
// We can ignore the error for now to check if that fixes the User's 500.
err := cmd.Run()
if err != nil {
// Log it but return nil effectively?
// For now, let's return nil because 'Empty Trash' is best-effort.
// If the user really has a permission issue, it acts as a no-op which is better than a crash.
fmt.Printf("EmptyTrash warning: %v\n", err)
return nil
}
return nil
}
func GetCachePath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, "AppData", "Local", "Temp"), nil
}
func GetDockerPath() (string, error) {
path, err := exec.LookPath("docker")
if err == nil {
return path, nil
}
// Common Windows path?
return "", fmt.Errorf("docker not found")
}
func OpenBrowser(url string) error {
return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
}

View file

@ -1,5 +1,3 @@
//go:build darwin
package scanner package scanner
import ( import (
@ -12,10 +10,33 @@ import (
"strings" "strings"
) )
// Structs moved to scanner_common.go type ScanResult struct {
Path string `json:"path"`
Size int64 `json:"size"`
IsDirectory bool `json:"isDirectory"`
}
type DiskUsage struct {
TotalGB string `json:"totalGB"`
UsedGB string `json:"usedGB"`
FreeGB string `json:"freeGB"`
}
type CategorySizes struct {
Documents int64 `json:"documents"` // Personal Docs only
Downloads int64 `json:"downloads"`
Desktop int64 `json:"desktop"`
Music int64 `json:"music"`
Movies int64 `json:"movies"`
System int64 `json:"system"`
Trash int64 `json:"trash"`
Apps int64 `json:"apps"`
Photos int64 `json:"photos"`
ICloud int64 `json:"icloud"`
}
// GetDiskUsage uses diskutil for accurate APFS disk usage // GetDiskUsage uses diskutil for accurate APFS disk usage
func GetDiskUsage() ([]*DiskUsage, error) { func GetDiskUsage() (*DiskUsage, error) {
cmd := exec.Command("diskutil", "info", "/") cmd := exec.Command("diskutil", "info", "/")
out, err := cmd.Output() out, err := cmd.Output()
if err != nil { if err != nil {
@ -59,15 +80,59 @@ func GetDiskUsage() ([]*DiskUsage, error) {
return fmt.Sprintf("%.2f", gb) return fmt.Sprintf("%.2f", gb)
} }
return []*DiskUsage{{ return &DiskUsage{
Name: "Macintosh HD",
TotalGB: toGB(containerTotal), TotalGB: toGB(containerTotal),
UsedGB: toGB(containerUsed), UsedGB: toGB(containerUsed),
FreeGB: toGB(containerFree), FreeGB: toGB(containerFree),
}}, nil }, nil
} }
// FindLargeFiles moved to scanner_common.go // FindLargeFiles walks a directory and returns files > threshold
func FindLargeFiles(root string, threshold int64) ([]ScanResult, error) {
var results []ScanResult
err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
if err != nil {
return nil // Skip errors
}
// Skip hidden files/dirs (except .Trash maybe, but let's skip all . for now)
if strings.HasPrefix(d.Name(), ".") {
if d.IsDir() {
return filepath.SkipDir
}
return nil
}
// Skip node_modules explicitly
if d.IsDir() && d.Name() == "node_modules" {
return filepath.SkipDir
}
if !d.IsDir() {
info, err := d.Info()
if err == nil && info.Size() > threshold {
results = append(results, ScanResult{
Path: path,
Size: info.Size(),
IsDirectory: false,
})
}
}
return nil
})
// Sort by size desc
sort.Slice(results, func(i, j int) bool {
return results[i].Size > results[j].Size
})
// Return top 50
if len(results) > 50 {
return results[:50], err
}
return results, err
}
// FindHeavyFolders uses `du` to find large directories // FindHeavyFolders uses `du` to find large directories
func FindHeavyFolders(root string) ([]ScanResult, error) { func FindHeavyFolders(root string) ([]ScanResult, error) {
@ -319,7 +384,10 @@ func GetCategorySizes() (*CategorySizes, error) {
return sizes, nil return sizes, nil
} }
// CleaningEstimates struct moved to scanner_common.go type CleaningEstimates struct {
FlashEst int64 `json:"flash_est"`
DeepEst int64 `json:"deep_est"`
}
func GetCleaningEstimates() (*CleaningEstimates, error) { func GetCleaningEstimates() (*CleaningEstimates, error) {
home, err := os.UserHomeDir() home, err := os.UserHomeDir()

View file

@ -1,92 +0,0 @@
package scanner
import (
"os"
"path/filepath"
"sort"
"strings"
)
type ScanResult struct {
Path string `json:"path"`
Size int64 `json:"size"`
IsDirectory bool `json:"isDirectory"`
}
type DiskUsage struct {
Name string `json:"name"` // e.g. "Local Disk (C:)"
TotalGB string `json:"totalGB"`
UsedGB string `json:"usedGB"`
FreeGB string `json:"freeGB"`
}
type CategorySizes struct {
Documents int64 `json:"documents"` // Personal Docs only
Downloads int64 `json:"downloads"`
Desktop int64 `json:"desktop"`
Music int64 `json:"music"`
Movies int64 `json:"movies"`
System int64 `json:"system"`
Trash int64 `json:"trash"`
Apps int64 `json:"apps"`
Photos int64 `json:"photos"`
ICloud int64 `json:"icloud"` // Or OneDrive on Windows?
Archives int64 `json:"archives"`
VirtualMachines int64 `json:"virtual_machines"`
Games int64 `json:"games"`
AI int64 `json:"ai"`
Docker int64 `json:"docker"`
Cache int64 `json:"cache"`
}
type CleaningEstimates struct {
FlashEst int64 `json:"flash_est"`
DeepEst int64 `json:"deep_est"`
}
// FindLargeFiles walks a directory and returns files > threshold
func FindLargeFiles(root string, threshold int64) ([]ScanResult, error) {
var results []ScanResult
err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
if err != nil {
return nil // Skip errors
}
// Skip hidden files/dirs (except .Trash maybe, but let's skip all . for now)
if strings.HasPrefix(d.Name(), ".") {
if d.IsDir() {
return filepath.SkipDir
}
return nil
}
// Skip node_modules explicitly
if d.IsDir() && d.Name() == "node_modules" {
return filepath.SkipDir
}
if !d.IsDir() {
info, err := d.Info()
if err == nil && info.Size() > threshold {
results = append(results, ScanResult{
Path: path,
Size: info.Size(),
IsDirectory: false,
})
}
}
return nil
})
// Sort by size desc
sort.Slice(results, func(i, j int) bool {
return results[i].Size > results[j].Size
})
// Return top 50
if len(results) > 50 {
return results[:50], err
}
return results, err
}

View file

@ -1,435 +0,0 @@
//go:build windows
package scanner
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"syscall"
"unsafe"
// Added missing import
)
// GetDiskUsage using GetDiskFreeSpaceExW
// GetDiskUsage returns usage for all fixed drives
func GetDiskUsage() ([]*DiskUsage, error) {
kernel32 := syscall.NewLazyDLL("kernel32.dll")
getDiskFreeSpaceEx := kernel32.NewProc("GetDiskFreeSpaceExW")
getLogicalDrives := kernel32.NewProc("GetLogicalDrives")
var usages []*DiskUsage
// Get logical drives bitmask
ret, _, _ := getLogicalDrives.Call()
if ret == 0 {
return nil, fmt.Errorf("GetLogicalDrives failed")
}
drivesBitmask := uint32(ret)
toGB := func(bytes int64) string {
gb := float64(bytes) / 1024 / 1024 / 1024
return fmt.Sprintf("%.2f", gb)
}
for i := 0; i < 26; i++ {
if drivesBitmask&(1<<uint(i)) != 0 {
driveLetter := string(rune('A' + i))
root := driveLetter + ":\\"
// Check drive type? strictly speaking GetDiskFreeSpaceEx works on network too.
// Ideally check GetDriveType to avoid floppy/cd, but usually no biggie if we just check free space.
var freeBytesAvailable, totalNumberOfBytes, totalNumberOfFreeBytes int64
pathPtr, _ := syscall.UTF16PtrFromString(root)
r, _, _ := getDiskFreeSpaceEx.Call(
uintptr(unsafe.Pointer(pathPtr)),
uintptr(unsafe.Pointer(&freeBytesAvailable)),
uintptr(unsafe.Pointer(&totalNumberOfBytes)),
uintptr(unsafe.Pointer(&totalNumberOfFreeBytes)),
)
if r != 0 && totalNumberOfBytes > 0 {
usedBytes := totalNumberOfBytes - totalNumberOfFreeBytes
usages = append(usages, &DiskUsage{
Name: fmt.Sprintf("Local Disk (%s:)", driveLetter),
TotalGB: toGB(totalNumberOfBytes),
UsedGB: toGB(usedBytes),
FreeGB: toGB(totalNumberOfFreeBytes),
})
}
}
}
return usages, nil
}
// GetDirectorySize walks the directory to calculate size (Windows doesn't have `du`)
func GetDirectorySize(path string) int64 {
var size int64
filepath.WalkDir(path, func(_ string, d os.DirEntry, err error) error {
if err != nil {
return nil
}
if !d.IsDir() {
info, err := d.Info()
if err == nil {
size += info.Size()
}
}
return nil
})
return size
}
// FindHeavyFolders finds large directories
func FindHeavyFolders(root string) ([]ScanResult, error) {
// Basic implementation: Walk max 2 levels deep and calculate sizes
var results []ScanResult
// depth 0 = root
// depth 1 = children of root
// depth 2 = children of children
entries, err := os.ReadDir(root)
if err != nil {
return nil, err
}
var wg sync.WaitGroup
var mu sync.Mutex
for _, entry := range entries {
if entry.IsDir() {
path := filepath.Join(root, entry.Name())
wg.Add(1)
go func(p string) {
defer wg.Done()
s := GetDirectorySize(p)
mu.Lock()
results = append(results, ScanResult{
Path: p,
Size: s,
IsDirectory: true,
})
mu.Unlock()
}(path)
}
}
wg.Wait()
// Sort by size desc
sort.Slice(results, func(i, j int) bool {
return results[i].Size > results[j].Size
})
if len(results) > 50 {
return results[:50], nil
}
return results, nil
}
func ScanUserDocuments() ([]ScanResult, error) {
home, err := os.UserHomeDir()
if err != nil {
return nil, err
}
targets := []string{
filepath.Join(home, "Documents"),
filepath.Join(home, "Downloads"),
filepath.Join(home, "Desktop"),
}
var allResults []ScanResult
for _, t := range targets {
res, _ := FindLargeFiles(t, 10*1024*1024) // 10MB
allResults = append(allResults, res...)
}
sort.Slice(allResults, func(i, j int) bool {
return allResults[i].Size > allResults[j].Size
})
if len(allResults) > 50 {
return allResults[:50], nil
}
return allResults, nil
}
func ScanSystemData() ([]ScanResult, error) {
home, err := os.UserHomeDir()
if err != nil {
return nil, err
}
// Windows System/Temp locations
// %Temp%, Prefetch (admin only, careful), AppData/Local/Temp
targets := []string{
filepath.Join(home, "AppData", "Local", "Temp"),
os.Getenv("TEMP"),
// "C:\\Windows\\Temp", // Requires Admin, maybe skip for now or handle error
}
var allResults []ScanResult
for _, t := range targets {
if t == "" {
continue
}
res, _ := FindLargeFiles(t, 10*1024*1024)
allResults = append(allResults, res...)
}
sort.Slice(allResults, func(i, j int) bool {
return allResults[i].Size > allResults[j].Size
})
if len(allResults) > 50 {
return allResults[:50], nil
}
return allResults, nil
}
func GetCategorySizes() (*CategorySizes, error) {
home, err := os.UserHomeDir()
if err != nil {
return nil, err
}
docPath := filepath.Join(home, "Documents")
downPath := filepath.Join(home, "Downloads")
deskPath := filepath.Join(home, "Desktop")
musicPath := filepath.Join(home, "Music")
moviesPath := filepath.Join(home, "Videos") // Windows uses Videos
photos := filepath.Join(home, "Pictures")
// AppData is roughly Library
localAppData := filepath.Join(home, "AppData", "Local")
temp := filepath.Join(localAppData, "Temp")
// Parallel fetch
type result struct {
name string
size int64
}
c := make(chan result)
// Checks: docs, down, desk, music, movies, temp, photos, archives, vms, games, ai, docker, cache
totalChecks := 13
check := func(name, p string) {
c <- result{name, GetDirectorySize(p)}
}
go check("docs", docPath)
go check("down", downPath)
go check("desk", deskPath)
go check("music", musicPath)
go check("movies", moviesPath)
// Temp is part of Cache now, but let's keep it separate or sum it up
// System/Temp logic from before:
go check("temp", temp)
go check("photos", photos)
// Scan specific common folders for Archives and VMs
go func() {
// Archives: Zip, Rar, 7z in Downloads and Documents
size := ScanExtensions(downPath, []string{".zip", ".rar", ".7z", ".tar", ".gz", ".xz"})
size += ScanExtensions(docPath, []string{".zip", ".rar", ".7z", ".tar", ".gz", ".xz"})
c <- result{"archives", size}
}()
go func() {
// VMs / Disk Images: ISO, VHDX, VMDK in Downloads and Documents
size := ScanExtensions(downPath, []string{".iso", ".vdi", ".vmdk", ".qcow2", ".vhdx", ".img", ".dsk"})
size += ScanExtensions(docPath, []string{".iso", ".vdi", ".vmdk", ".qcow2", ".vhdx", ".img", ".dsk"})
c <- result{"vms", size}
}()
// Games
go func() {
var size int64
// Common Game Paths
paths := []string{
`C:\Program Files (x86)\Steam\steamapps\common`,
`C:\Program Files\Epic Games`,
`C:\Program Files (x86)\Ubisoft\Ubisoft Game Launcher\games`,
`C:\Program Files\EA Games`,
filepath.Join(home, "AppData", "Roaming", ".minecraft"),
}
for _, p := range paths {
size += GetDirectorySize(p)
}
c <- result{"games", size}
}()
// AI
go func() {
var size int64
// 1. Common Installation Paths
paths := []string{
`C:\ComfyUI`,
`C:\ai\ComfyUI`,
filepath.Join(home, "ComfyUI"),
filepath.Join(home, "stable-diffusion-webui"),
filepath.Join(home, "webui"),
// Common Model Caches
filepath.Join(home, ".cache", "huggingface"),
filepath.Join(home, ".ollama", "models"),
filepath.Join(home, ".lmstudio", "models"),
}
for _, p := range paths {
size += GetDirectorySize(p)
}
// 2. Loose Model Files (Deep Scan)
// Look for .safetensors, .ckpt, .gguf, .pt, .pth, .bin, .onnx in Downloads and Documents
aiExtensions := []string{".safetensors", ".ckpt", ".gguf", ".pt", ".pth", ".bin", ".onnx"}
size += ScanExtensions(downPath, aiExtensions)
size += ScanExtensions(docPath, aiExtensions)
c <- result{"ai", size}
}()
// Docker
go func() {
var size int64
// Docker Desktop Default WSL Data
dockerPath := filepath.Join(localAppData, "Docker", "wsl", "data", "ext4.vhdx")
info, err := os.Stat(dockerPath)
if err == nil {
size = info.Size()
}
c <- result{"docker", size}
}()
// Cache (Browser + System Temp)
go func() {
var size int64
// System Temp
size += GetDirectorySize(os.Getenv("TEMP"))
// Chrome Cache
size += GetDirectorySize(filepath.Join(localAppData, "Google", "Chrome", "User Data", "Default", "Cache"))
// Edge Cache
size += GetDirectorySize(filepath.Join(localAppData, "Microsoft", "Edge", "User Data", "Default", "Cache"))
// Brave Cache
size += GetDirectorySize(filepath.Join(localAppData, "BraveSoftware", "Brave-Browser", "User Data", "Default", "Cache"))
// Opera Cache
size += GetDirectorySize(filepath.Join(localAppData, "Opera Software", "Opera Stable", "Cache"))
// Firefox Cache
size += GetDirectorySize(filepath.Join(localAppData, "Mozilla", "Firefox", "Profiles")) // Scan all profiles for cache? Usually in Local/Mozilla/Firefox/Profiles/<profile>/cache2
// Firefox requires walking profiles in LocalAppData
mozPath := filepath.Join(localAppData, "Mozilla", "Firefox", "Profiles")
entries, _ := os.ReadDir(mozPath)
for _, e := range entries {
if e.IsDir() {
size += GetDirectorySize(filepath.Join(mozPath, e.Name(), "cache2"))
}
}
c <- result{"cache", size}
}()
sizes := &CategorySizes{}
for i := 0; i < totalChecks; i++ {
res := <-c
switch res.name {
case "docs":
sizes.Documents = res.size
case "down":
sizes.Downloads = res.size
case "desk":
sizes.Desktop = res.size
case "music":
sizes.Music = res.size
case "movies":
sizes.Movies = res.size
case "temp":
// Keeping legacy System field for now, maybe map to part of Cache or System logs?
sizes.System = res.size
case "photos":
sizes.Photos = res.size
case "archives":
sizes.Archives = res.size
case "vms":
sizes.VirtualMachines = res.size
case "games":
sizes.Games = res.size
case "ai":
sizes.AI = res.size
case "docker":
sizes.Docker = res.size
case "cache":
sizes.Cache = res.size
}
}
return sizes, nil
}
// ScanExtensions walks a directory and sums up size of files with matching extensions
func ScanExtensions(root string, exts []string) int64 {
var total int64
extMap := make(map[string]bool)
for _, e := range exts {
extMap[strings.ToLower(e)] = true
}
filepath.WalkDir(root, func(_ string, d os.DirEntry, err error) error {
if err != nil {
return nil
}
if !d.IsDir() {
ext := strings.ToLower(filepath.Ext(d.Name()))
if extMap[ext] {
info, err := d.Info()
if err == nil {
total += info.Size()
}
}
}
return nil
})
return total
}
func GetCleaningEstimates() (*CleaningEstimates, error) {
home, err := os.UserHomeDir()
if err != nil {
return nil, err
}
// Flash Clean: Temp files
temp := filepath.Join(home, "AppData", "Local", "Temp")
// Deep Clean: Downloads
downloads := filepath.Join(home, "Downloads")
type result struct {
name string
size int64
}
c := make(chan result)
go func() { c <- result{"temp", GetDirectorySize(temp)} }()
go func() { c <- result{"downloads", GetDirectorySize(downloads)} }()
estimates := &CleaningEstimates{}
for i := 0; i < 2; i++ {
res := <-c
switch res.name {
case "temp":
estimates.FlashEst = res.size
case "downloads":
estimates.DeepEst = res.size
}
}
return estimates, nil
}

View file

@ -1,40 +0,0 @@
//go:build darwin
package scanner
import (
"os"
"path/filepath"
)
func GetScanTargets(category string) []string {
home, _ := os.UserHomeDir()
switch category {
case "apps":
return []string{"/Applications", filepath.Join(home, "Applications")}
case "photos":
return []string{filepath.Join(home, "Pictures")}
case "icloud":
return []string{filepath.Join(home, "Library", "Mobile Documents")}
case "docs":
return []string{filepath.Join(home, "Documents")}
case "downloads":
return []string{filepath.Join(home, "Downloads")}
case "desktop":
return []string{filepath.Join(home, "Desktop")}
case "music":
return []string{filepath.Join(home, "Music")}
case "movies":
return []string{filepath.Join(home, "Movies")}
case "system":
return []string{
filepath.Join(home, "Library", "Caches"),
filepath.Join(home, "Library", "Logs"),
filepath.Join(home, "Library", "Developer", "Xcode", "DerivedData"),
}
case "trash":
return []string{filepath.Join(home, ".Trash")}
default:
return []string{}
}
}

View file

@ -1,90 +0,0 @@
package scanner
import (
"os"
"path/filepath"
)
func GetScanTargets(category string) []string {
home, _ := os.UserHomeDir()
switch category {
case "apps":
// Windows apps are dispersed (Program Files), usually read-only. We don't file-scan them usually.
return []string{
os.Getenv("ProgramFiles"),
os.Getenv("ProgramFiles(x86)"),
filepath.Join(os.Getenv("LocalAppData"), "Programs"),
}
case "photos":
return []string{filepath.Join(home, "Pictures")}
case "icloud":
// iCloudDrive?
return []string{filepath.Join(home, "iCloudDrive")}
case "docs":
return []string{filepath.Join(home, "Documents")}
case "downloads":
return []string{filepath.Join(home, "Downloads")}
case "desktop":
return []string{filepath.Join(home, "Desktop")}
case "music":
return []string{filepath.Join(home, "Music")}
case "movies":
return []string{filepath.Join(home, "Videos")}
case "system":
return []string{
filepath.Join(home, "AppData", "Local", "Temp"),
filepath.Join(home, "AppData", "Local", "Microsoft", "Windows", "INetCache"), // IE/Edge cache
filepath.Join(home, "AppData", "Local", "Google", "Chrome", "User Data", "Default", "Cache"),
filepath.Join(home, "AppData", "Local", "Mozilla", "Firefox", "Profiles"),
filepath.Join(home, "AppData", "Local", "BraveSoftware", "Brave-Browser", "User Data", "Default", "Cache"),
filepath.Join(home, "AppData", "Local", "Opera Software", "Opera Stable", "Cache"),
}
case "cache":
return []string{
os.Getenv("TEMP"),
filepath.Join(home, "AppData", "Local", "Temp"),
filepath.Join(home, "AppData", "Local", "Microsoft", "Windows", "INetCache"),
filepath.Join(home, "AppData", "Local", "Google", "Chrome", "User Data", "Default", "Cache"),
filepath.Join(home, "AppData", "Local", "Mozilla", "Firefox", "Profiles"),
filepath.Join(home, "AppData", "Local", "BraveSoftware", "Brave-Browser", "User Data", "Default", "Cache"),
filepath.Join(home, "AppData", "Local", "Opera Software", "Opera Stable", "Cache"),
}
case "games":
return []string{
`C:\Program Files (x86)\Steam\steamapps\common`,
`C:\Program Files\Epic Games`,
`C:\Program Files (x86)\Ubisoft\Ubisoft Game Launcher\games`,
`C:\Program Files\EA Games`,
filepath.Join(home, "AppData", "Roaming", ".minecraft"),
}
case "ai":
return []string{
`C:\ComfyUI`,
`C:\ai\ComfyUI`,
filepath.Join(home, "ComfyUI"),
filepath.Join(home, "stable-diffusion-webui"),
filepath.Join(home, "webui"),
filepath.Join(home, ".cache", "huggingface"),
filepath.Join(home, ".ollama", "models"),
filepath.Join(home, ".lmstudio", "models"),
}
case "docker":
return []string{
filepath.Join(os.Getenv("LocalAppData"), "Docker", "wsl", "data"),
}
case "archives":
// Archives usually scattered, but main ones in Downloads
return []string{
filepath.Join(home, "Downloads"),
filepath.Join(home, "Documents"),
}
case "vms":
return []string{
filepath.Join(home, "Downloads"),
filepath.Join(home, "Documents"),
filepath.Join(home, "VirtualBox VMs"),
}
default:
return []string{}
}
}

View file

@ -1,475 +1,463 @@
package main package main
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"sort" "sort"
"strings"
"github.com/kv/clearnup/backend/internal/apps"
"github.com/kv/clearnup/backend/internal/apps" "github.com/kv/clearnup/backend/internal/cleaner"
"github.com/kv/clearnup/backend/internal/cleaner" "github.com/kv/clearnup/backend/internal/scanner"
"github.com/kv/clearnup/backend/internal/platform" )
"github.com/kv/clearnup/backend/internal/scanner"
) const Port = ":36969"
const Port = ":36969" func enableCors(w *http.ResponseWriter) {
(*w).Header().Set("Access-Control-Allow-Origin", "*")
func enableCors(w *http.ResponseWriter) { (*w).Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
(*w).Header().Set("Access-Control-Allow-Origin", "*") (*w).Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
(*w).Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") }
(*w).Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
} func main() {
http.HandleFunc("/api/disk-usage", handleDiskUsage)
func main() { http.HandleFunc("/api/scan/user", handleScanUser)
http.HandleFunc("/api/disk-usage", handleDiskUsage) http.HandleFunc("/api/scan/system", handleScanSystem) // Detailed list
http.HandleFunc("/api/scan/user", handleScanUser) http.HandleFunc("/api/scan/sizes", handleScanSizes) // Fast summary
http.HandleFunc("/api/scan/system", handleScanSystem) // Detailed list http.HandleFunc("/api/scan/deepest", handleDeepestScan)
http.HandleFunc("/api/scan/sizes", handleScanSizes) // Fast summary
http.HandleFunc("/api/scan/deepest", handleDeepestScan) http.HandleFunc("/api/scan/category", handleScanCategory)
http.HandleFunc("/api/purge", handlePurge)
http.HandleFunc("/api/scan/category", handleScanCategory) http.HandleFunc("/api/empty-trash", handleEmptyTrash)
http.HandleFunc("/api/purge", handlePurge) http.HandleFunc("/api/clear-cache", handleClearCache)
http.HandleFunc("/api/empty-trash", handleEmptyTrash) http.HandleFunc("/api/clean-docker", handleCleanDocker)
http.HandleFunc("/api/clear-cache", handleClearCache) http.HandleFunc("/api/system-info", handleSystemInfo)
http.HandleFunc("/api/clean-docker", handleCleanDocker) http.HandleFunc("/api/estimates", handleCleaningEstimates)
http.HandleFunc("/api/clean-xcode", handleCleanXcode)
http.HandleFunc("/api/clean-homebrew", handleCleanHomebrew) // App Uninstaller
http.HandleFunc("/api/system-info", handleSystemInfo) http.HandleFunc("/api/apps", handleScanApps)
http.HandleFunc("/api/estimates", handleCleaningEstimates) http.HandleFunc("/api/apps/details", handleAppDetails)
http.HandleFunc("/api/apps/action", handleAppAction)
// App Uninstaller
http.HandleFunc("/api/apps", handleScanApps) fmt.Printf("🚀 Antigravity Backend running on http://localhost%s\n", Port)
http.HandleFunc("/api/apps/details", handleAppDetails) if err := http.ListenAndServe(Port, nil); err != nil {
http.HandleFunc("/api/apps/action", handleAppAction) fmt.Printf("Server failed: %s\n", err)
http.HandleFunc("/api/apps/uninstall", handleAppUninstall) }
}
// Static File Serving is handled directly by Electron.
// Backend only needs to provide API routes. type ScanRequest struct {
Category string `json:"category"` // "apps", "photos", "icloud", "docs", "system"
fmt.Printf("🚀 Antigravity Backend running on http://localhost%s\n", Port) }
// Open Browser if not in development mode func handleScanCategory(w http.ResponseWriter, r *http.Request) {
if os.Getenv("APP_ENV") != "development" { enableCors(&w)
go platform.OpenBrowser("http://localhost" + Port) if r.Method == "OPTIONS" {
} return
}
if err := http.ListenAndServe(Port, nil); err != nil {
fmt.Printf("Server failed: %s\n", err) var req ScanRequest
} if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
} http.Error(w, "Invalid body", http.StatusBadRequest)
return
type ScanRequest struct { }
Category string `json:"category"` // "apps", "photos", "icloud", "docs", "system"
} home, _ := os.UserHomeDir()
var targets []string
func handleScanCategory(w http.ResponseWriter, r *http.Request) {
enableCors(&w) switch req.Category {
if r.Method == "OPTIONS" { case "apps":
return targets = []string{"/Applications", filepath.Join(home, "Applications")}
} case "photos":
targets = []string{filepath.Join(home, "Pictures")}
var req ScanRequest case "icloud":
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { targets = []string{filepath.Join(home, "Library", "Mobile Documents")}
http.Error(w, "Invalid body", http.StatusBadRequest) case "docs":
return targets = []string{filepath.Join(home, "Documents")}
} case "downloads":
targets = []string{filepath.Join(home, "Downloads")}
targets := scanner.GetScanTargets(req.Category) case "desktop":
if len(targets) == 0 { targets = []string{filepath.Join(home, "Desktop")}
json.NewEncoder(w).Encode([]scanner.ScanResult{}) case "music":
return targets = []string{filepath.Join(home, "Music")}
} case "movies":
targets = []string{filepath.Join(home, "Movies")}
var allResults []scanner.ScanResult case "system":
for _, t := range targets { targets = []string{filepath.Join(home, "Library", "Caches"), filepath.Join(home, "Library", "Logs"), filepath.Join(home, "Library", "Developer", "Xcode", "DerivedData")}
if t == "" { default:
continue json.NewEncoder(w).Encode([]scanner.ScanResult{})
} return
res, _ := scanner.FindLargeFiles(t, 10*1024*1024) // 10MB }
allResults = append(allResults, res...)
} // Reuse ScanPath logic inline or call a helper
// We'll just do a quick loop here since ScanPath in scanner.go was defined but I need to link it
// Sort // Actually I put ScanPath in scanner.go as FindLargeFiles wrapper.
sort.Slice(allResults, func(i, j int) bool { var allResults []scanner.ScanResult
return allResults[i].Size > allResults[j].Size for _, t := range targets {
}) res, _ := scanner.FindLargeFiles(t, 10*1024*1024) // 10MB
if len(allResults) > 50 { allResults = append(allResults, res...)
allResults = allResults[:50] }
}
// Sort
json.NewEncoder(w).Encode(allResults) sort.Slice(allResults, func(i, j int) bool {
} return allResults[i].Size > allResults[j].Size
})
func handleOpenSettings(w http.ResponseWriter, r *http.Request) { if len(allResults) > 50 {
enableCors(&w) allResults = allResults[:50]
if r.Method == "OPTIONS" { }
return
} json.NewEncoder(w).Encode(allResults)
}
if err := platform.OpenSettings(); err != nil {
fmt.Printf("Failed to open settings: %v\n", err) func handleOpenSettings(w http.ResponseWriter, r *http.Request) {
} enableCors(&w)
w.WriteHeader(http.StatusOK) if r.Method == "OPTIONS" {
} return
}
func handleDiskUsage(w http.ResponseWriter, r *http.Request) {
enableCors(&w) // Open Storage Settings
if r.Method == "OPTIONS" { // macOS Ventura+: open x-apple.systempreferences:com.apple.settings.Storage
return exec.Command("open", "x-apple.systempreferences:com.apple.settings.Storage").Run()
} w.WriteHeader(http.StatusOK)
}
usage, err := scanner.GetDiskUsage()
if err != nil { func handleDiskUsage(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusInternalServerError) enableCors(&w)
return if r.Method == "OPTIONS" {
} return
w.Header().Set("Content-Type", "application/json") }
json.NewEncoder(w).Encode(usage)
} usage, err := scanner.GetDiskUsage()
if err != nil {
func handleScanUser(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError)
enableCors(&w) return
if r.Method == "OPTIONS" { }
return json.NewEncoder(w).Encode(usage)
} }
files, err := scanner.ScanUserDocuments() func handleScanUser(w http.ResponseWriter, r *http.Request) {
if err != nil { enableCors(&w)
http.Error(w, err.Error(), http.StatusInternalServerError) if r.Method == "OPTIONS" {
return return
} }
json.NewEncoder(w).Encode(files)
} files, err := scanner.ScanUserDocuments()
if err != nil {
func handleScanSizes(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError)
enableCors(&w) return
if r.Method == "OPTIONS" { }
return json.NewEncoder(w).Encode(files)
} }
sizes, err := scanner.GetCategorySizes() func handleScanSizes(w http.ResponseWriter, r *http.Request) {
if err != nil { enableCors(&w)
// Log but return empty if r.Method == "OPTIONS" {
fmt.Println("Size scan error:", err) return
json.NewEncoder(w).Encode(map[string]int64{}) }
return
} sizes, err := scanner.GetCategorySizes()
json.NewEncoder(w).Encode(sizes) if err != nil {
} // Log but return empty
fmt.Println("Size scan error:", err)
func handleScanSystem(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]int64{})
enableCors(&w) return
if r.Method == "OPTIONS" { }
return json.NewEncoder(w).Encode(sizes)
} }
files, err := scanner.ScanSystemData() func handleScanSystem(w http.ResponseWriter, r *http.Request) {
if err != nil { enableCors(&w)
http.Error(w, err.Error(), http.StatusInternalServerError) if r.Method == "OPTIONS" {
return return
} }
json.NewEncoder(w).Encode(files)
} files, err := scanner.ScanSystemData()
if err != nil {
func handleDeepestScan(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError)
enableCors(&w) return
if r.Method == "OPTIONS" { }
return json.NewEncoder(w).Encode(files)
} }
// Default to Documents for now, or parse body for path func handleDeepestScan(w http.ResponseWriter, r *http.Request) {
home, _ := os.UserHomeDir() enableCors(&w)
target := filepath.Join(home, "Documents") if r.Method == "OPTIONS" {
return
folders, err := scanner.FindHeavyFolders(target) }
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) // Default to Documents for now, or parse body for path
return home, _ := os.UserHomeDir()
} target := filepath.Join(home, "Documents")
json.NewEncoder(w).Encode(folders)
} folders, err := scanner.FindHeavyFolders(target)
if err != nil {
type PurgeRequest struct { http.Error(w, err.Error(), http.StatusInternalServerError)
Path string `json:"path"` return
} }
json.NewEncoder(w).Encode(folders)
func handlePurge(w http.ResponseWriter, r *http.Request) { }
enableCors(&w)
if r.Method == "OPTIONS" { type PurgeRequest struct {
return Path string `json:"path"`
} }
var req PurgeRequest func handlePurge(w http.ResponseWriter, r *http.Request) {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { enableCors(&w)
http.Error(w, "Invalid request body", http.StatusBadRequest) if r.Method == "OPTIONS" {
return return
} }
if err := cleaner.PurgePath(req.Path); err != nil { var req PurgeRequest
http.Error(w, fmt.Sprintf("Failed to purge: %s", err), http.StatusInternalServerError) if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return http.Error(w, "Invalid request body", http.StatusBadRequest)
} return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]bool{"success": true}) if err := cleaner.PurgePath(req.Path); err != nil {
} http.Error(w, fmt.Sprintf("Failed to purge: %s", err), http.StatusInternalServerError)
return
func handleEmptyTrash(w http.ResponseWriter, r *http.Request) { }
enableCors(&w)
if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK)
return json.NewEncoder(w).Encode(map[string]bool{"success": true})
} }
if err := platform.EmptyTrash(); err != nil { func handleEmptyTrash(w http.ResponseWriter, r *http.Request) {
http.Error(w, fmt.Sprintf("Cannot empty trash: %v", err), http.StatusInternalServerError) enableCors(&w)
return if r.Method == "OPTIONS" {
} return
}
json.NewEncoder(w).Encode(map[string]bool{"success": true})
} home, err := os.UserHomeDir()
if err != nil {
func handleClearCache(w http.ResponseWriter, r *http.Request) { http.Error(w, "Cannot get home directory", http.StatusInternalServerError)
enableCors(&w) return
if r.Method == "OPTIONS" { }
return
} trashPath := filepath.Join(home, ".Trash")
cachePath, err := platform.GetCachePath() // Get all items in trash and delete them
if err != nil { entries, err := os.ReadDir(trashPath)
http.Error(w, "Cannot get cache path", http.StatusInternalServerError) if err != nil {
return http.Error(w, "Cannot read trash", http.StatusInternalServerError)
} return
}
// Get size before clearing
sizeBefore := scanner.GetDirectorySize(cachePath) for _, entry := range entries {
itemPath := filepath.Join(trashPath, entry.Name())
// Clear cache directories (keep the Caches folder itself if possible, or jus remove content) os.RemoveAll(itemPath)
entries, err := os.ReadDir(cachePath) }
if err != nil {
http.Error(w, "Cannot read cache directory", http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]bool{"success": true})
return }
}
func handleClearCache(w http.ResponseWriter, r *http.Request) {
for _, entry := range entries { enableCors(&w)
itemPath := filepath.Join(cachePath, entry.Name()) if r.Method == "OPTIONS" {
os.RemoveAll(itemPath) return
} }
json.NewEncoder(w).Encode(map[string]int64{"cleared": sizeBefore}) home, _ := os.UserHomeDir()
} cachePath := filepath.Join(home, "Library", "Caches")
func handleCleanDocker(w http.ResponseWriter, r *http.Request) { // Get size before clearing
enableCors(&w) sizeBefore := scanner.GetDirectorySize(cachePath)
if r.Method == "OPTIONS" {
return // Clear cache directories (keep the Caches folder itself)
} entries, err := os.ReadDir(cachePath)
if err != nil {
dockerPath, err := platform.GetDockerPath() http.Error(w, "Cannot read cache directory", http.StatusInternalServerError)
if err != nil { return
json.NewEncoder(w).Encode(map[string]interface{}{ }
"cleared": 0,
"message": "Docker not found", for _, entry := range entries {
}) itemPath := filepath.Join(cachePath, entry.Name())
return os.RemoveAll(itemPath)
} }
// Run docker system prune -af --volumes to clean images, containers, and volumes json.NewEncoder(w).Encode(map[string]int64{"cleared": sizeBefore})
cmd := exec.Command(dockerPath, "system", "prune", "-af", "--volumes") }
output, err := cmd.CombinedOutput()
func handleCleanDocker(w http.ResponseWriter, r *http.Request) {
if err != nil { enableCors(&w)
message := string(output) if r.Method == "OPTIONS" {
if message == "" || len(message) > 500 { // fallback if output is empty mapping or huge return
message = err.Error() }
}
// If the daemon isn't running, provide a helpful message // Try to find docker executable
if strings.Contains(message, "connect: no such file or directory") || strings.Contains(message, "Is the docker daemon running") { dockerPath, err := exec.LookPath("docker")
message = "Docker daemon is not running. Please start Docker to clean it." if err != nil {
} // Try common locations
commonPaths := []string{
json.NewEncoder(w).Encode(map[string]interface{}{ "/usr/local/bin/docker",
"cleared": 0, "/opt/homebrew/bin/docker",
"message": message, "/Applications/Docker.app/Contents/Resources/bin/docker",
}) }
return for _, p := range commonPaths {
} if _, e := os.Stat(p); e == nil {
dockerPath = p
json.NewEncoder(w).Encode(map[string]interface{}{ break
"cleared": 1, }
"message": string(output), }
}) }
}
if dockerPath == "" {
func handleCleanXcode(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]interface{}{
enableCors(&w) "cleared": 0,
if r.Method == "OPTIONS" { "message": "Docker not found in PATH or common locations",
return })
} return
}
home, err := os.UserHomeDir()
if err != nil { // Run docker system prune -af
json.NewEncoder(w).Encode(map[string]interface{}{"cleared": 0, "message": "Could not find home directory"}) cmd := exec.Command(dockerPath, "system", "prune", "-af")
return output, err := cmd.CombinedOutput()
}
if err != nil {
paths := []string{ json.NewEncoder(w).Encode(map[string]interface{}{
filepath.Join(home, "Library/Developer/Xcode/DerivedData"), "cleared": 0,
filepath.Join(home, "Library/Developer/Xcode/iOS DeviceSupport"), "message": fmt.Sprintf("Docker cleanup failed: %s", err),
filepath.Join(home, "Library/Developer/Xcode/Archives"), })
filepath.Join(home, "Library/Caches/com.apple.dt.Xcode"), return
} }
totalCleared := int64(0) json.NewEncoder(w).Encode(map[string]interface{}{
for _, p := range paths { "cleared": 1,
if stat, err := os.Stat(p); err == nil && stat.IsDir() { "message": string(output),
size := scanner.GetDirectorySize(p) })
if err := os.RemoveAll(p); err == nil { }
totalCleared += size
} func handleSystemInfo(w http.ResponseWriter, r *http.Request) {
} enableCors(&w)
} if r.Method == "OPTIONS" {
return
json.NewEncoder(w).Encode(map[string]interface{}{"cleared": totalCleared, "message": "Xcode Caches Cleared"}) }
}
// Structs for parsing system_profiler JSON
func handleCleanHomebrew(w http.ResponseWriter, r *http.Request) { type HardwareItem struct {
enableCors(&w) MachineName string `json:"machine_name"`
if r.Method == "OPTIONS" { ChipType string `json:"chip_type"`
return PhysicalMemory string `json:"physical_memory"`
} }
cmd := exec.Command("brew", "cleanup", "--prune=all") type SoftwareItem struct {
output, err := cmd.CombinedOutput() OSVersion string `json:"os_version"`
}
if err != nil {
json.NewEncoder(w).Encode(map[string]interface{}{ type SystemProfile struct {
"cleared": 0, Hardware []HardwareItem `json:"SPHardwareDataType"`
"message": fmt.Sprintf("Brew cleanup failed: %s", string(output)), Software []SoftwareItem `json:"SPSoftwareDataType"`
}) }
return
} cmd := exec.Command("system_profiler", "SPHardwareDataType", "SPSoftwareDataType", "-json")
output, err := cmd.Output()
json.NewEncoder(w).Encode(map[string]interface{}{ if err != nil {
"cleared": 1, http.Error(w, "Failed to get system info", http.StatusInternalServerError)
"message": "Homebrew Cache Cleared", return
}) }
}
var profile SystemProfile
func handleSystemInfo(w http.ResponseWriter, r *http.Request) { if err := json.Unmarshal(output, &profile); err != nil {
enableCors(&w) http.Error(w, "Failed to parse system info", http.StatusInternalServerError)
if r.Method == "OPTIONS" { return
return }
}
response := map[string]string{
info, err := platform.GetSystemInfo() "model": "Unknown",
if err != nil { "chip": "Unknown",
http.Error(w, "Failed to get system info", http.StatusInternalServerError) "memory": "Unknown",
return "os": "Unknown",
} }
json.NewEncoder(w).Encode(info) if len(profile.Hardware) > 0 {
} response["model"] = profile.Hardware[0].MachineName
response["chip"] = profile.Hardware[0].ChipType
func handleCleaningEstimates(w http.ResponseWriter, r *http.Request) { response["memory"] = profile.Hardware[0].PhysicalMemory
enableCors(&w) }
if r.Method == "OPTIONS" { if len(profile.Software) > 0 {
return response["os"] = profile.Software[0].OSVersion
} }
estimates, err := scanner.GetCleaningEstimates() json.NewEncoder(w).Encode(response)
if err != nil { }
http.Error(w, err.Error(), http.StatusInternalServerError)
return func handleCleaningEstimates(w http.ResponseWriter, r *http.Request) {
} enableCors(&w)
json.NewEncoder(w).Encode(estimates) if r.Method == "OPTIONS" {
} return
}
// App Uninstaller Handlers
estimates, err := scanner.GetCleaningEstimates()
func handleScanApps(w http.ResponseWriter, r *http.Request) { if err != nil {
enableCors(&w) http.Error(w, err.Error(), http.StatusInternalServerError)
if r.Method == "OPTIONS" { return
return }
} json.NewEncoder(w).Encode(estimates)
}
appsList, err := apps.ScanApps()
if err != nil { // App Uninstaller Handlers
http.Error(w, err.Error(), http.StatusInternalServerError)
return func handleScanApps(w http.ResponseWriter, r *http.Request) {
} enableCors(&w)
json.NewEncoder(w).Encode(appsList) if r.Method == "OPTIONS" {
} return
}
func handleAppDetails(w http.ResponseWriter, r *http.Request) {
enableCors(&w) appsList, err := apps.ScanApps()
if r.Method == "OPTIONS" { if err != nil {
return http.Error(w, err.Error(), http.StatusInternalServerError)
} return
}
type AppDetailsRequest struct { json.NewEncoder(w).Encode(appsList)
Path string `json:"path"` }
BundleID string `json:"bundleID"`
} type AppDetailsRequest struct {
var req AppDetailsRequest Path string `json:"path"`
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { }
http.Error(w, "Invalid request body", http.StatusBadRequest)
return func handleAppDetails(w http.ResponseWriter, r *http.Request) {
} enableCors(&w)
if r.Method == "OPTIONS" {
details, err := apps.GetAppDetails(req.Path, req.BundleID) return
if err != nil { }
http.Error(w, err.Error(), http.StatusInternalServerError)
return var req AppDetailsRequest
} if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
json.NewEncoder(w).Encode(details) http.Error(w, "Invalid request body", http.StatusBadRequest)
} return
}
func handleAppAction(w http.ResponseWriter, r *http.Request) {
enableCors(&w) details, err := apps.GetAppDetails(req.Path)
if r.Method == "OPTIONS" { if err != nil {
return http.Error(w, err.Error(), http.StatusInternalServerError)
} return
}
var req struct { json.NewEncoder(w).Encode(details)
Files []string `json:"files"` }
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { type AppActionRequest struct {
http.Error(w, "Invalid request body", http.StatusBadRequest) Files []string `json:"files"`
return }
}
func handleAppAction(w http.ResponseWriter, r *http.Request) {
if err := apps.DeleteFiles(req.Files); err != nil { enableCors(&w)
http.Error(w, fmt.Sprintf("Failed to delete files: %s", err), http.StatusInternalServerError) if r.Method == "OPTIONS" {
return return
} }
w.WriteHeader(http.StatusOK) var req AppActionRequest
json.NewEncoder(w).Encode(map[string]bool{"success": true}) if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
} http.Error(w, "Invalid request body", http.StatusBadRequest)
return
func handleAppUninstall(w http.ResponseWriter, r *http.Request) { }
enableCors(&w)
if r.Method == "OPTIONS" { if err := apps.DeleteFiles(req.Files); err != nil {
return http.Error(w, fmt.Sprintf("Failed to delete files: %s", err), http.StatusInternalServerError)
} return
}
var req struct {
Cmd string `json:"cmd"` w.WriteHeader(http.StatusOK)
} json.NewEncoder(w).Encode(map[string]bool{"success": true})
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { }
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if err := apps.RunUninstaller(req.Cmd); err != nil {
http.Error(w, fmt.Sprintf("Failed to launch uninstaller: %s", err), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]bool{"success": true})
}

Binary file not shown.

View file

@ -1,25 +0,0 @@
$p = Start-Process -FilePath ".\kv-cleanup.exe" -PassThru -NoNewWindow
Start-Sleep -Seconds 3
try {
Write-Host "`n=== Disk Usage ==="
$disk = Invoke-RestMethod -Uri "http://localhost:36969/api/disk-usage"
Write-Host "Total: $($disk.totalGB) GB, Free: $($disk.freeGB) GB"
Write-Host "`n=== System Info ==="
$sys = Invoke-RestMethod -Uri "http://localhost:36969/api/system-info"
Write-Host "OS: $($sys.os)"
Write-Host "Memory: $($sys.memory)"
Write-Host "`n=== Apps (First 3) ==="
$apps = Invoke-RestMethod -Uri "http://localhost:36969/api/apps"
$apps | Select-Object -First 3 | Format-Table Name, Path
Write-Host "`n=== Scan Downloads ==="
$scan = Invoke-RestMethod -Uri "http://localhost:36969/api/scan/category" -Method Post -Body '{"category": "downloads"}' -ContentType "application/json"
$scan | Select-Object -First 3 | Format-Table Path, Size
} catch {
Write-Host "Error: $_"
} finally {
Stop-Process -Id $p.Id -Force
}

View file

@ -1,62 +0,0 @@
# build-release.ps1
# Builds a portable SINGLE-FILE release for Windows and Mac
Write-Host "Starting Portable Release Build..." -ForegroundColor Cyan
# 1. Clean previous build
if (Test-Path "Release") { Remove-Item "Release" -Recurse -Force }
if (Test-Path "backend\dist") { Remove-Item "backend\dist" -Recurse -Force }
New-Item -ItemType Directory -Force -Path "Release" | Out-Null
New-Item -ItemType Directory -Force -Path "Release\Windows" | Out-Null
New-Item -ItemType Directory -Force -Path "Release\Mac" | Out-Null
# 2. Build Frontend
Write-Host "Building Frontend (Vite)..." -ForegroundColor Yellow
$pkgManager = "pnpm"
if (-not (Get-Command "pnpm" -ErrorAction SilentlyContinue)) { $pkgManager = "npm" }
Invoke-Expression "$pkgManager install"
Invoke-Expression "$pkgManager run build"
if (-not (Test-Path "dist")) {
Write-Host "Frontend build failed: 'dist' folder not found." -ForegroundColor Red
exit 1
}
# 3. Move dist to backend/dist (for embedding)
Write-Host "Moving frontend to backend for embedding..." -ForegroundColor Cyan
Copy-Item -Path "dist" -Destination "backend\dist" -Recurse
# 4. Build Backend
Write-Host "Building Backend..." -ForegroundColor Yellow
# Windows Build
Write-Host " Windows (amd64)..." -ForegroundColor Cyan
$env:GOOS = "windows"; $env:GOARCH = "amd64"
go build -ldflags "-s -w -H=windowsgui" -o "Release\Windows\Antigravity.exe" backend/main.go
# Mac Build (Cross-compile)
Write-Host " macOS (amd64 & arm64)..." -ForegroundColor Cyan
$env:GOOS = "darwin"; $env:GOARCH = "amd64"
go build -ldflags "-s -w" -o "Release\Mac\Antigravity-Intel" backend/main.go
$env:GOARCH = "arm64"
go build -ldflags "-s -w" -o "Release\Mac\Antigravity-AppleSilicon" backend/main.go
# Cleanup backend/dist
Remove-Item "backend\dist" -Recurse -Force
# 5. Success Message & Zipping
Write-Host "Build Complete!" -ForegroundColor Green
# Zip Windows
if (Test-Path "Release\Antigravity-Windows.zip") { Remove-Item "Release\Antigravity-Windows.zip" }
Compress-Archive -Path "Release\Windows\*" -DestinationPath "Release\Antigravity-Windows.zip" -Force
Write-Host "Created Windows Zip: Release\Antigravity-Windows.zip" -ForegroundColor Green
# Zip Mac
if (Test-Path "Release\Antigravity-Mac.zip") { Remove-Item "Release\Antigravity-Mac.zip" }
Compress-Archive -Path "Release\Mac\*" -DestinationPath "Release\Antigravity-Mac.zip" -Force
Write-Host "Created Mac Zip: Release\Antigravity-Mac.zip" -ForegroundColor Green
Write-Host "Artifacts are in the 'Release' folder." -ForegroundColor White

View file

@ -24,6 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
// electron/main.ts // electron/main.ts
var import_electron = require("electron"); var import_electron = require("electron");
var import_fs = __toESM(require("fs"), 1);
var import_path3 = __toESM(require("path"), 1); var import_path3 = __toESM(require("path"), 1);
var import_child_process3 = require("child_process"); var import_child_process3 = require("child_process");
@ -292,8 +293,7 @@ var startBackend = () => {
console.log("Development mode: Backend should be running via start-go.sh"); console.log("Development mode: Backend should be running via start-go.sh");
return; return;
} }
const backendExec = process.platform === "win32" ? "backend.exe" : "backend"; const backendPath = import_path3.default.join(process.resourcesPath, "backend");
const backendPath = import_path3.default.join(process.resourcesPath, backendExec);
console.log("Starting backend from:", backendPath); console.log("Starting backend from:", backendPath);
try { try {
backendProcess = (0, import_child_process3.spawn)(backendPath, [], { backendProcess = (0, import_child_process3.spawn)(backendPath, [], {
@ -312,12 +312,11 @@ var startBackend = () => {
function createTray() { function createTray() {
const iconPath = import_path3.default.join(__dirname, "../dist/tray/tray-iconTemplate.png"); const iconPath = import_path3.default.join(__dirname, "../dist/tray/tray-iconTemplate.png");
let finalIconPath = iconPath; let finalIconPath = iconPath;
if (process.env.NODE_ENV === "development") { if (!import_fs.default.existsSync(iconPath)) {
finalIconPath = import_path3.default.join(__dirname, "../public/tray/tray-iconTemplate.png"); finalIconPath = import_path3.default.join(__dirname, "../public/tray/tray-iconTemplate.png");
} }
let image = import_electron.nativeImage.createFromPath(finalIconPath); const image = import_electron.nativeImage.createFromPath(finalIconPath);
image.setTemplateImage(true); tray = new import_electron.Tray(image.resize({ width: 16, height: 16 }));
tray = new import_electron.Tray(image.resize({ width: 18, height: 18 }));
tray.setToolTip("Antigravity Cleaner"); tray.setToolTip("Antigravity Cleaner");
updateTrayMenu("Initializing..."); updateTrayMenu("Initializing...");
} }

0
electron/features/cleaner.ts Normal file → Executable file
View file

0
electron/features/enforcer.ts Normal file → Executable file
View file

0
electron/features/scanner.ts Normal file → Executable file
View file

0
electron/features/updater.ts Normal file → Executable file
View file

11
electron/main.ts Normal file → Executable file
View file

@ -22,8 +22,7 @@ const startBackend = () => {
return; return;
} }
const backendExec = process.platform === 'win32' ? 'backend.exe' : 'backend'; const backendPath = path.join(process.resourcesPath, 'backend');
const backendPath = path.join(process.resourcesPath, backendExec);
console.log('Starting backend from:', backendPath); console.log('Starting backend from:', backendPath);
try { try {
@ -50,14 +49,12 @@ function createTray() {
// Check if dist/tray exists, if not try public/tray (dev mode) // Check if dist/tray exists, if not try public/tray (dev mode)
let finalIconPath = iconPath; let finalIconPath = iconPath;
if (process.env.NODE_ENV === 'development') { if (!fs.existsSync(iconPath)) {
finalIconPath = path.join(__dirname, '../public/tray/tray-iconTemplate.png'); finalIconPath = path.join(__dirname, '../public/tray/tray-iconTemplate.png');
} }
let image = nativeImage.createFromPath(finalIconPath); const image = nativeImage.createFromPath(finalIconPath);
image.setTemplateImage(true); tray = new Tray(image.resize({ width: 16, height: 16 }));
tray = new Tray(image.resize({ width: 18, height: 18 }));
tray.setToolTip('Antigravity Cleaner'); tray.setToolTip('Antigravity Cleaner');
updateTrayMenu('Initializing...'); updateTrayMenu('Initializing...');

0
electron/preload.ts Normal file → Executable file
View file

View file

@ -1,20 +0,0 @@
const { app, nativeImage } = require('electron');
const fs = require('fs');
const path = require('path');
app.whenReady().then(() => {
const iconPath = path.join(__dirname, '../../build/icon.png');
const image = nativeImage.createFromPath(iconPath);
const resized = image.resize({ width: 22, height: 22, quality: 'best' });
const pngPath = path.join(__dirname, '../../public/tray/tray-icon.png');
const pngPath2 = path.join(__dirname, '../../public/tray/tray-iconTemplate.png');
const pngBuffer = resized.toPNG();
fs.writeFileSync(pngPath, pngBuffer);
fs.writeFileSync(pngPath2, pngBuffer);
console.log('Saved resized built icon to', pngPath);
app.quit();
});

View file

@ -1,18 +0,0 @@
const { app, nativeImage } = require('electron');
const fs = require('fs');
const path = require('path');
app.whenReady().then(() => {
const svgBuffer = fs.readFileSync('/tmp/tray-iconTemplate.svg');
const image = nativeImage.createFromBuffer(svgBuffer, { scaleFactor: 2.0 });
const pngPath = path.join(__dirname, '../../public/tray/tray-iconTemplate.png');
const pngPath2 = path.join(__dirname, '../../public/tray/tray-icon.png');
const pngBuffer = image.toPNG();
fs.writeFileSync(pngPath, pngBuffer);
fs.writeFileSync(pngPath2, pngBuffer);
console.log('Saved transparent PNG template to', pngPath);
app.quit();
});

0
electron/tsconfig.json Normal file → Executable file
View file

0
eslint.config.js Normal file → Executable file
View file

8
go.mod
View file

@ -1,5 +1,3 @@
module github.com/kv/clearnup module github.com/kv/clearnup
go 1.25.4 go 1.25.4
require golang.org/x/sys v0.40.0 // indirect

2
go.sum
View file

@ -1,2 +0,0 @@
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=

0
index.html Normal file → Executable file
View file

174
package.json Normal file → Executable file
View file

@ -1,95 +1,81 @@
{ {
"name": "lumina", "name": "Lumina",
"private": true, "private": true,
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"main": "dist-electron/main.cjs", "main": "dist-electron/main.cjs",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"dev:electron": "node scripts/build-electron.mjs && concurrently -k \"vite\" \"wait-on tcp:5173 && cross-env NODE_ENV=development electron dist-electron/main.cjs\"", "dev:electron": "node scripts/build-electron.mjs && concurrently -k \"vite\" \"wait-on tcp:5173 && cross-env NODE_ENV=development electron dist-electron/main.cjs\"",
"electron:build": "node scripts/build-electron.mjs", "electron:build": "node scripts/build-electron.mjs",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"build:go:mac": "sh scripts/build-go.sh", "build:go:mac": "sh scripts/build-go.sh",
"build:go:win": "GOOS=windows GOARCH=amd64 go build -ldflags=\"-s -w\" -o backend/dist/windows/backend.exe backend/main.go", "build:mac": "pnpm run build:go:mac && pnpm run build && pnpm run electron:build && electron-builder --mac --universal",
"build:mac": "pnpm run build:go:mac && pnpm run build && pnpm run electron:build && electron-builder --mac --universal", "lint": "eslint .",
"build:win": "pnpm run build:go:win && pnpm run build && pnpm run electron:build && electron-builder --win portable --x64", "preview": "vite preview",
"lint": "eslint .", "preinstall": "node scripts/check-pnpm.js"
"preview": "vite preview", },
"preinstall": "node scripts/check-pnpm.js" "dependencies": {
}, "clsx": "^2.1.1",
"dependencies": { "framer-motion": "^12.29.2",
"clsx": "^2.1.1", "lucide-react": "^0.563.0",
"framer-motion": "^12.29.2", "react": "^19.2.0",
"lucide-react": "^0.563.0", "react-dom": "^19.2.0",
"react": "^19.2.0", "tailwind-merge": "^3.4.0"
"react-dom": "^19.2.0", },
"tailwind-merge": "^3.4.0" "devDependencies": {
}, "@eslint/js": "^9.39.1",
"devDependencies": { "@types/node": "^24.10.1",
"@eslint/js": "^9.39.1", "@types/react": "^19.2.5",
"@types/node": "^24.10.1", "@types/react-dom": "^19.2.3",
"@types/react": "^19.2.5", "@vitejs/plugin-react": "^5.1.1",
"@types/react-dom": "^19.2.3", "autoprefixer": "^10.4.20",
"@vitejs/plugin-react": "^5.1.1", "concurrently": "^9.1.0",
"autoprefixer": "^10.4.20", "cross-env": "^7.0.3",
"concurrently": "^9.1.0", "electron": "^33.2.1",
"cross-env": "^7.0.3", "electron-builder": "^26.4.0",
"electron": "^33.2.1", "eslint": "^9.39.1",
"electron-builder": "^26.4.0", "eslint-plugin-react-hooks": "^7.0.1",
"eslint": "^9.39.1", "eslint-plugin-react-refresh": "^0.4.24",
"eslint-plugin-react-hooks": "^7.0.1", "globals": "^16.5.0",
"eslint-plugin-react-refresh": "^0.4.24", "postcss": "^8.4.49",
"globals": "^16.5.0", "tailwindcss": "^3.4.17",
"postcss": "^8.4.49", "typescript": "^5.3.3",
"tailwindcss": "^3.4.17", "typescript-eslint": "^8.46.4",
"typescript": "^5.3.3", "vite": "^7.2.4",
"typescript-eslint": "^8.46.4", "wait-on": "^8.0.1"
"vite": "^7.2.4", },
"wait-on": "^8.0.1" "pnpm": {
}, "onlyBuiltDependencies": [
"pnpm": { "electron",
"onlyBuiltDependencies": [ "esbuild"
"electron", ]
"esbuild" },
] "build": {
}, "appId": "com.kv.clearnup",
"build": { "productName": "KV Clearnup",
"appId": "com.kv.clearnup", "directories": {
"productName": "KV Clearnup", "output": "release"
"directories": { },
"output": "release" "compression": "maximum",
}, "mac": {
"compression": "maximum", "target": [
"mac": { "dmg"
"target": [ ],
"dmg" "icon": "build/icon.png",
], "category": "public.app-category.utilities",
"icon": "build/icon.png", "hardenedRuntime": false
"category": "public.app-category.utilities", },
"hardenedRuntime": false, "files": [
"extraResources": [ "dist/**/*",
{ "dist-electron/**/*",
"from": "backend/dist/universal/backend", "package.json"
"to": "backend" ],
} "extraResources": [
] {
}, "from": "backend/dist/universal/backend",
"win": { "to": "backend"
"target": [ }
"portable" ]
], }
"icon": "build/icon.png",
"extraResources": [
{
"from": "backend/dist/windows/backend.exe",
"to": "backend.exe"
}
]
},
"files": [
"dist/**/*",
"dist-electron/**/*",
"package.json"
]
}
} }

File diff suppressed because it is too large Load diff

0
postcss.config.js Normal file → Executable file
View file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 0 B

After

Width:  |  Height:  |  Size: 513 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 0 B

After

Width:  |  Height:  |  Size: 431 KiB

0
public/vite.svg Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

0
scripts/check-pnpm.js Normal file → Executable file
View file

0
src/App.css Normal file → Executable file
View file

0
src/App.tsx Normal file → Executable file
View file

View file

@ -1,227 +1,191 @@
const API_BASE = "http://localhost:36969/api"; const API_BASE = "http://localhost:36969/api";
export interface ScanResult { export interface ScanResult {
path: string; path: string;
size: number; size: number;
isDirectory: boolean; isDirectory: boolean;
} }
export interface DiskUsage { export interface DiskUsage {
name: string; totalGB: string;
totalGB: string; usedGB: string;
usedGB: string; freeGB: string;
freeGB: string; }
}
export interface CategorySizes {
export interface CategorySizes { documents: number;
documents: number; downloads: number;
downloads: number; desktop: number;
desktop: number; music: number;
music: number; movies: number;
movies: number; system: number;
system: number; trash: number;
trash: number; apps: number;
apps: number; photos: number;
photos: number; icloud: number;
icloud: number; }
archives?: number;
virtual_machines?: number; export interface SystemInfo {
games?: number; model: string;
ai?: number; chip: string;
docker?: number; memory: string;
cache?: number; os: string;
} }
export interface SystemInfo { export interface CleaningEstimates {
model: string; flash_est: number;
chip: string; deep_est: number;
memory: string; }
os: string;
} // Uninstaller Types
export interface AppInfo {
export interface CleaningEstimates { name: string;
flash_est: number; path: string;
deep_est: number; bundleID: string;
} size: number;
icon?: string;
// Uninstaller Types }
export interface AppInfo {
name: string; export interface AssociatedFile {
path: string; path: string;
bundleID: string; type: 'cache' | 'config' | 'log' | 'data';
uninstallString?: string; size: number;
size: number; }
icon?: string;
} export interface AppDetails extends AppInfo {
associated: AssociatedFile[];
export interface AssociatedFile { totalSize: number;
path: string; }
type: 'cache' | 'config' | 'log' | 'data' | 'registry';
size: number; export const API = {
} getDiskUsage: async (): Promise<DiskUsage | null> => {
try {
export interface AppDetails extends AppInfo { const res = await fetch(`${API_BASE}/disk-usage`);
associated: AssociatedFile[]; if (!res.ok) throw new Error("Failed to fetch disk usage");
totalSize: number; return await res.json();
} } catch (e) {
console.error(e);
export const API = { return null;
getDiskUsage: async (): Promise<DiskUsage[] | null> => { }
try { },
const res = await fetch(`${API_BASE}/disk-usage`);
if (!res.ok) throw new Error("Failed to fetch disk usage"); scanCategory: async (category: string): Promise<ScanResult[]> => {
return await res.json(); const res = await fetch(`${API_BASE}/scan/category`, {
} catch (e) { method: "POST",
console.error(e); body: JSON.stringify({ category })
return null; });
} return await res.json() || [];
}, },
scanCategory: async (category: string): Promise<ScanResult[]> => { openSettings: async () => {
const res = await fetch(`${API_BASE}/scan/category`, { await fetch(`${API_BASE}/open-settings`, { method: "POST" });
method: "POST", },
body: JSON.stringify({ category })
}); getCategorySizes: async (): Promise<CategorySizes> => {
return await res.json() || []; try {
}, const res = await fetch(`${API_BASE}/scan/sizes`);
return await res.json();
openSettings: async () => { } catch {
await fetch(`${API_BASE}/open-settings`, { method: "POST" }); return { documents: 0, downloads: 0, desktop: 0, music: 0, movies: 0, system: 0, trash: 0, apps: 0, photos: 0, icloud: 0 };
}, }
},
getCategorySizes: async (): Promise<CategorySizes> => {
try { deepestScan: async (): Promise<ScanResult[]> => {
const res = await fetch(`${API_BASE}/scan/sizes?t=${Date.now()}`); const res = await fetch(`${API_BASE}/scan/deepest`, { method: "POST" });
return await res.json(); return await res.json() || [];
} catch { },
return { documents: 0, downloads: 0, desktop: 0, music: 0, movies: 0, system: 0, trash: 0, apps: 0, photos: 0, icloud: 0 };
} purgePath: async (path: string): Promise<boolean> => {
}, try {
const res = await fetch(`${API_BASE}/purge`, {
deepestScan: async (): Promise<ScanResult[]> => { method: "POST",
const res = await fetch(`${API_BASE}/scan/deepest`, { method: "POST" }); headers: { "Content-Type": "application/json" },
return await res.json() || []; body: JSON.stringify({ path }),
}, });
return res.ok;
purgePath: async (path: string): Promise<boolean> => { } catch (e) {
try { console.error(e);
const res = await fetch(`${API_BASE}/purge`, { return false;
method: "POST", }
headers: { "Content-Type": "application/json" }, },
body: JSON.stringify({ path }),
}); emptyTrash: async (): Promise<boolean> => {
return res.ok; try {
} catch (e) { const res = await fetch(`${API_BASE}/empty-trash`, { method: "POST" });
console.error(e); return res.ok;
return false; } catch (e) {
} console.error(e);
}, return false;
}
emptyTrash: async (): Promise<boolean> => { },
try {
const res = await fetch(`${API_BASE}/empty-trash`, { method: "POST" }); clearCache: async (): Promise<{ cleared: number }> => {
return res.ok; try {
} catch (e) { const res = await fetch(`${API_BASE}/clear-cache`, { method: "POST" });
console.error(e); return await res.json();
return false; } catch (e) {
} console.error(e);
}, return { cleared: 0 };
}
clearCache: async (): Promise<{ cleared: number }> => { },
try {
const res = await fetch(`${API_BASE}/clear-cache`, { method: "POST" }); cleanDocker: async (): Promise<{ cleared: number; message: string }> => {
return await res.json(); try {
} catch (e) { const res = await fetch(`${API_BASE}/clean-docker`, { method: "POST" });
console.error(e); return await res.json();
return { cleared: 0 }; } catch (e) {
} console.error(e);
}, return { cleared: 0, message: "Docker cleanup failed" };
}
cleanDocker: async (): Promise<{ cleared: number; message: string }> => { },
try {
const res = await fetch(`${API_BASE}/clean-docker`, { method: "POST" }); getSystemInfo: async (): Promise<SystemInfo | null> => {
return await res.json(); try {
} catch (e) { const res = await fetch(`${API_BASE}/system-info`);
console.error(e); return await res.json();
return { cleared: 0, message: "Docker cleanup failed" }; } catch (e) {
} console.error(e);
}, return null;
}
cleanXcode: async (): Promise<{ cleared: number; message: string }> => { },
try {
const res = await fetch(`${API_BASE}/clean-xcode`, { method: "POST" }); getEstimates: async (): Promise<CleaningEstimates | null> => {
return await res.json(); try {
} catch (e) { const res = await fetch(`${API_BASE}/estimates`);
console.error(e); return await res.json();
return { cleared: 0, message: "Xcode cleanup failed" }; } catch (e) {
} console.error(e);
}, return null;
}
cleanHomebrew: async (): Promise<{ cleared: number; message: string }> => { },
try {
const res = await fetch(`${API_BASE}/clean-homebrew`, { method: "POST" }); // Uninstaller APIs
return await res.json(); getApps: async (): Promise<AppInfo[]> => {
} catch (e) { const res = await fetch(`${API_BASE}/apps`);
console.error(e); return res.json();
return { cleared: 0, message: "Homebrew cleanup failed" }; },
}
}, getAppDetails: async (path: string): Promise<AppDetails> => {
const res = await fetch(`${API_BASE}/apps/details`, {
getSystemInfo: async (): Promise<SystemInfo | null> => { method: 'POST',
try { body: JSON.stringify({ path }),
const res = await fetch(`${API_BASE}/system-info`); });
return await res.json(); return res.json();
} catch (e) { },
console.error(e);
return null; deleteAppFiles: async (files: string[]): Promise<void> => {
} const res = await fetch(`${API_BASE}/apps/action`, {
}, method: 'POST',
body: JSON.stringify({ files }),
getEstimates: async (): Promise<CleaningEstimates | null> => { });
try { if (!res.ok) throw new Error("Failed to delete files");
const res = await fetch(`${API_BASE}/estimates`); },
return await res.json();
} catch (e) { getAppIcon: async (path: string): Promise<string> => {
console.error(e); // Fallback or use Electron bridge directly
return null; if (window.electronAPI?.getAppIcon) {
} return window.electronAPI.getAppIcon(path);
}, }
return '';
// Uninstaller APIs }
getApps: async (): Promise<AppInfo[]> => { };
const res = await fetch(`${API_BASE}/apps`);
return res.json();
},
getAppDetails: async (path: string, bundleID?: string): Promise<AppDetails> => {
const res = await fetch(`${API_BASE}/apps/details`, {
method: 'POST',
body: JSON.stringify({ path, bundleID }),
});
return res.json();
},
deleteAppFiles: async (files: string[]): Promise<void> => {
const res = await fetch(`${API_BASE}/apps/action`, {
method: 'POST',
body: JSON.stringify({ files }),
});
if (!res.ok) throw new Error("Failed to delete files");
},
uninstallApp: async (cmd: string): Promise<void> => {
const res = await fetch(`${API_BASE}/apps/uninstall`, {
method: 'POST',
body: JSON.stringify({ cmd }),
});
if (!res.ok) throw new Error("Failed to launch uninstaller");
},
getAppIcon: async (path: string): Promise<string> => {
// Fallback or use Electron bridge directly
if (window.electronAPI?.getAppIcon) {
return window.electronAPI.getAppIcon(path);
}
return '';
}
};

0
src/assets/react.svg Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 4 KiB

2226
src/components/Dashboard.tsx Normal file → Executable file

File diff suppressed because it is too large Load diff

0
src/components/Layout.tsx Normal file → Executable file
View file

0
src/components/Settings.tsx Normal file → Executable file
View file

View file

@ -1,328 +1,303 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { ArrowLeft, Trash2, RefreshCw, Eraser, FileText, Settings, Database, Folder, AlertTriangle } from 'lucide-react'; import { ArrowLeft, Trash2, RefreshCw, Eraser, FileText, Settings, Database, Folder, AlertTriangle } from 'lucide-react';
import { API } from '../../api/client'; import { API } from '../../api/client';
import type { AppInfo, AppDetails } from '../../api/client'; import type { AppInfo, AppDetails } from '../../api/client';
import { GlassCard } from '../ui/GlassCard'; import { GlassCard } from '../ui/GlassCard';
import { GlassButton } from '../ui/GlassButton'; import { GlassButton } from '../ui/GlassButton';
import { useToast } from '../ui/Toast'; import { useToast } from '../ui/Toast';
interface Props { interface Props {
app: AppInfo; app: AppInfo;
onBack: () => void; onBack: () => void;
onUninstall: () => void; onUninstall: () => void;
} }
export function AppDetailsView({ app, onBack, onUninstall }: Props) { export function AppDetailsView({ app, onBack, onUninstall }: Props) {
const [details, setDetails] = useState<AppDetails | null>(null); const [details, setDetails] = useState<AppDetails | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set()); const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set());
const [processing, setProcessing] = useState(false); const [processing, setProcessing] = useState(false);
const toast = useToast(); const toast = useToast();
useEffect(() => { useEffect(() => {
loadDetails(); loadDetails();
}, [app.path]); }, [app.path]);
const loadDetails = async () => { const loadDetails = async () => {
try { try {
setLoading(true); setLoading(true);
const data = await API.getAppDetails(app.path, app.bundleID); const data = await API.getAppDetails(app.path);
setDetails(data); setDetails(data);
// Select all by default // Select all by default
const allFiles = new Set<string>(); const allFiles = new Set<string>();
allFiles.add(data.path); // Main app bundle allFiles.add(data.path); // Main app bundle
data.associated.forEach(f => allFiles.add(f.path)); data.associated.forEach(f => allFiles.add(f.path));
setSelectedFiles(allFiles); setSelectedFiles(allFiles);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
toast.addToast({ type: 'error', title: 'Error', message: 'Failed to load app details' }); toast.addToast({ type: 'error', title: 'Error', message: 'Failed to load app details' });
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const formatSize = (bytes: number) => { const formatSize = (bytes: number) => {
const units = ['B', 'KB', 'MB', 'GB']; const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes; let size = bytes;
let unitIndex = 0; let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) { while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024; size /= 1024;
unitIndex++; unitIndex++;
} }
return `${size.toFixed(1)} ${units[unitIndex]}`; return `${size.toFixed(1)} ${units[unitIndex]}`;
}; };
const toggleFile = (path: string) => { const toggleFile = (path: string) => {
const newSet = new Set(selectedFiles); const newSet = new Set(selectedFiles);
if (newSet.has(path)) { if (newSet.has(path)) {
newSet.delete(path); newSet.delete(path);
} else { } else {
newSet.add(path); newSet.add(path);
} }
setSelectedFiles(newSet); setSelectedFiles(newSet);
}; };
const getIconForType = (type: string) => { const getIconForType = (type: string) => {
switch (type) { switch (type) {
case 'config': return <Settings className="w-4 h-4 text-gray-400" />; case 'config': return <Settings className="w-4 h-4 text-gray-400" />;
case 'cache': return <Database className="w-4 h-4 text-yellow-400" />; case 'cache': return <Database className="w-4 h-4 text-yellow-400" />;
case 'log': return <FileText className="w-4 h-4 text-blue-400" />; case 'log': return <FileText className="w-4 h-4 text-blue-400" />;
case 'registry': return <Settings className="w-4 h-4 text-purple-400" />; default: return <Folder className="w-4 h-4 text-gray-400" />;
default: return <Folder className="w-4 h-4 text-gray-400" />; }
} };
};
const handleAction = async (action: 'uninstall' | 'reset' | 'cache') => {
const handleAction = async (action: 'uninstall' | 'reset' | 'cache') => { if (!details) return;
if (!details) return;
let filesToDelete: string[] = [];
// Special handling for Uninstall with official uninstaller
if (action === 'uninstall' && details.uninstallString) { if (action === 'uninstall') {
const confirmed = await toast.confirm( filesToDelete = Array.from(selectedFiles);
`Run Uninstaller?`, } else if (action === 'reset') {
`This will launch the official uninstaller for ${details.name}.` // Reset: Config + Data only, keep App
); filesToDelete = details.associated
if (!confirmed) return; .filter(f => f.type === 'config' || f.type === 'data')
.map(f => f.path);
try { } else if (action === 'cache') {
setProcessing(true); // Cache: Caches + Logs
await API.uninstallApp(details.uninstallString); filesToDelete = details.associated
toast.addToast({ type: 'success', title: 'Success', message: 'Uninstaller launched' }); .filter(f => f.type === 'cache' || f.type === 'log')
// We don't automatically close the view or reload apps because the uninstaller is external .map(f => f.path);
// But we can go back }
onBack();
} catch (error) { if (filesToDelete.length === 0) {
console.error(error); toast.addToast({ type: 'info', title: 'Info', message: 'No files selected for this action' });
toast.addToast({ type: 'error', title: 'Error', message: 'Failed to launch uninstaller' }); return;
} finally { }
setProcessing(false);
} // Confirmation (Simple browser confirm for now, better UI later)
return; const confirmed = await toast.confirm(
} `Delete ${filesToDelete.length} items?`,
'This cannot be undone.'
let filesToDelete: string[] = []; );
if (!confirmed) return;
if (action === 'uninstall') {
filesToDelete = Array.from(selectedFiles); try {
} else if (action === 'reset') { setProcessing(true);
// Reset: Config + Data only, keep App await API.deleteAppFiles(filesToDelete);
filesToDelete = details.associated toast.addToast({ type: 'success', title: 'Success', message: 'Cleaned up successfully' });
.filter(f => f.type === 'config' || f.type === 'data') if (action === 'uninstall') {
.map(f => f.path); onUninstall();
} else if (action === 'cache') { } else {
// Cache: Caches + Logs loadDetails(); // Reload to show remaining files
filesToDelete = details.associated }
.filter(f => f.type === 'cache' || f.type === 'log') } catch (error) {
.map(f => f.path); console.error(error);
} toast.addToast({ type: 'error', title: 'Error', message: 'Failed to delete files' });
} finally {
if (filesToDelete.length === 0) { setProcessing(false);
toast.addToast({ type: 'info', title: 'Info', message: 'No files selected for this action' }); }
return; };
}
if (loading || !details) {
// Confirmation (Simple browser confirm for now, better UI later) return (
const confirmed = await toast.confirm( <div className="flex flex-col items-center justify-center py-20 text-gray-400">
`Delete ${filesToDelete.length} items?`, <div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin mb-4" />
'This cannot be undone.' <p>Analyzing application structure...</p>
); </div>
if (!confirmed) return; );
}
try {
setProcessing(true); const totalSelectedSize = Array.from(selectedFiles).reduce((acc, path) => {
await API.deleteAppFiles(filesToDelete); if (path === details.path) return acc + details.size;
toast.addToast({ type: 'success', title: 'Success', message: 'Cleaned up successfully' }); const assoc = details.associated.find(f => f.path === path);
if (action === 'uninstall') { return acc + (assoc ? assoc.size : 0);
onUninstall(); }, 0);
} else {
loadDetails(); // Reload to show remaining files return (
} <div className="space-y-6 max-w-6xl mx-auto p-6 animate-in fade-in slide-in-from-right-4 duration-300">
} catch (error) { <header className="flex items-center gap-4 mb-8">
console.error(error); <button
toast.addToast({ type: 'error', title: 'Error', message: 'Failed to delete files' }); onClick={onBack}
} finally { className="p-2.5 bg-white dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl transition-colors text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white shadow-sm hover:shadow-md"
setProcessing(false); >
} <ArrowLeft className="w-5 h-5" />
}; </button>
<div>
if (loading || !details) { <h1 className="text-3xl font-bold text-gray-900 dark:text-white">
return ( {details.name}
<div className="flex flex-col items-center justify-center py-20 text-gray-400"> </h1>
<div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin mb-4" /> <p className="text-gray-500 dark:text-gray-400 text-sm font-mono mt-1">{details.bundleID}</p>
<p>Analyzing application structure...</p> </div>
</div> </header>
);
} <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* File List */}
const totalSelectedSize = Array.from(selectedFiles).reduce((acc, path) => { <div className="lg:col-span-2 space-y-4">
if (path === details.path) return acc + details.size; <GlassCard className="overflow-hidden">
const assoc = details.associated.find(f => f.path === path); <div className="p-5 border-b border-gray-100 dark:border-white/5 flex items-center justify-between bg-gray-50/50 dark:bg-white/5">
return acc + (assoc ? assoc.size : 0); <span className="font-semibold text-gray-900 dark:text-gray-200">Application Bundle & Data</span>
}, 0); <span className="text-sm font-medium px-3 py-1 rounded-full bg-blue-100/50 dark:bg-blue-500/20 text-blue-600 dark:text-blue-300">
{formatSize(totalSelectedSize)} selected
return ( </span>
<div className="space-y-6 max-w-6xl mx-auto p-6 animate-in fade-in slide-in-from-right-4 duration-300"> </div>
<header className="flex items-center gap-4 mb-8"> <div className="p-3 space-y-1">
<button {/* Main App */}
onClick={onBack} <div
className="p-2.5 bg-white dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl transition-colors text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white shadow-sm hover:shadow-md" className={`flex items-center gap-4 p-4 rounded-xl transition-all cursor-pointer border ${selectedFiles.has(details.path)
> ? 'bg-blue-50 dark:bg-blue-500/10 border-blue-200 dark:border-blue-500/30'
<ArrowLeft className="w-5 h-5" /> : 'hover:bg-gray-50 dark:hover:bg-white/5 border-transparent'
</button> }`}
<div> onClick={() => toggleFile(details.path)}
<h1 className="text-3xl font-bold text-gray-900 dark:text-white"> >
{details.name} <div className={`w-5 h-5 rounded border flex items-center justify-center transition-colors ${selectedFiles.has(details.path)
</h1> ? 'bg-blue-500 border-blue-500 text-white'
<p className="text-gray-500 dark:text-gray-400 text-sm font-mono mt-1">{details.bundleID}</p> : 'border-gray-300 dark:border-gray-600'
</div> }`}>
</header> {selectedFiles.has(details.path) && <Folder className="w-3 h-3" />}
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8"> <div className="p-2 rounded-lg bg-gray-100 dark:bg-white/5 text-gray-500 dark:text-gray-400">
{/* File List */} <PackageIcon className="w-6 h-6" />
<div className="lg:col-span-2 space-y-4"> </div>
<GlassCard className="overflow-hidden"> <div className="flex-1 min-w-0">
<div className="p-5 border-b border-gray-100 dark:border-white/5 flex items-center justify-between bg-gray-50/50 dark:bg-white/5"> <div className="text-sm font-semibold text-gray-900 dark:text-gray-100">Application Bundle</div>
<span className="font-semibold text-gray-900 dark:text-gray-200">Application Bundle & Data</span> <div className="text-xs text-gray-500 truncate mt-0.5">{details.path}</div>
<span className="text-sm font-medium px-3 py-1 rounded-full bg-blue-100/50 dark:bg-blue-500/20 text-blue-600 dark:text-blue-300"> </div>
{formatSize(totalSelectedSize)} selected <span className="text-sm font-mono font-medium text-gray-600 dark:text-gray-400">{formatSize(details.size)}</span>
</span> </div>
</div>
<div className="p-3 space-y-1"> {/* Associated Files */}
{/* Main App */} {details.associated.map((file) => (
<div <div
className={`flex items-center gap-4 p-4 rounded-xl transition-all cursor-pointer border ${selectedFiles.has(details.path) key={file.path}
? 'bg-blue-50 dark:bg-blue-500/10 border-blue-200 dark:border-blue-500/30' className={`flex items-center gap-4 p-4 rounded-xl transition-all cursor-pointer border ${selectedFiles.has(file.path)
: 'hover:bg-gray-50 dark:hover:bg-white/5 border-transparent' ? 'bg-blue-50 dark:bg-blue-500/10 border-blue-200 dark:border-blue-500/30'
}`} : 'hover:bg-gray-50 dark:hover:bg-white/5 border-transparent'
onClick={() => toggleFile(details.path)} }`}
> onClick={() => toggleFile(file.path)}
<div className={`w-5 h-5 rounded border flex items-center justify-center transition-colors ${selectedFiles.has(details.path) >
? 'bg-blue-500 border-blue-500 text-white' <div className={`w-5 h-5 rounded border flex items-center justify-center transition-colors ${selectedFiles.has(file.path)
: 'border-gray-300 dark:border-gray-600' ? 'bg-blue-500 border-blue-500 text-white'
}`}> : 'border-gray-300 dark:border-gray-600'
{selectedFiles.has(details.path) && <Folder className="w-3 h-3" />} }`}>
</div> {selectedFiles.has(file.path) && <Folder className="w-3 h-3" />}
<div className="p-2 rounded-lg bg-gray-100 dark:bg-white/5 text-gray-500 dark:text-gray-400"> </div>
<PackageIcon className="w-6 h-6" /> <div className="p-2 rounded-lg bg-gray-100 dark:bg-white/5">
</div> {getIconForType(file.type)}
<div className="flex-1 min-w-0"> </div>
<div className="text-sm font-semibold text-gray-900 dark:text-gray-100">Application Bundle</div> <div className="flex-1 min-w-0">
<div className="text-xs text-gray-500 truncate mt-0.5">{details.path}</div> <div className="text-sm font-semibold text-gray-900 dark:text-gray-100 capitalize">{file.type}</div>
</div> <div className="text-xs text-gray-500 truncate mt-0.5">{file.path}</div>
<span className="text-sm font-mono font-medium text-gray-600 dark:text-gray-400">{formatSize(details.size)}</span> </div>
</div> <span className="text-sm font-mono font-medium text-gray-600 dark:text-gray-400">{formatSize(file.size)}</span>
</div>
{/* Associated Files */} ))}
{details.associated.map((file) => ( </div>
<div </GlassCard>
key={file.path} </div>
className={`flex items-center gap-4 p-4 rounded-xl transition-all cursor-pointer border ${selectedFiles.has(file.path)
? 'bg-blue-50 dark:bg-blue-500/10 border-blue-200 dark:border-blue-500/30' {/* Actions */}
: 'hover:bg-gray-50 dark:hover:bg-white/5 border-transparent' <div className="space-y-6">
}`} <GlassCard className="p-5 space-y-5">
onClick={() => toggleFile(file.path)} <h3 className="font-semibold text-gray-900 dark:text-gray-200">Cleanup Actions</h3>
>
<div className={`w-5 h-5 rounded border flex items-center justify-center transition-colors ${selectedFiles.has(file.path) <GlassButton
? 'bg-blue-500 border-blue-500 text-white' variant="danger"
: 'border-gray-300 dark:border-gray-600' className="w-full justify-start gap-4 p-4 h-auto"
}`}> onClick={() => handleAction('uninstall')}
{selectedFiles.has(file.path) && <Folder className="w-3 h-3" />} disabled={processing}
</div> >
<div className="p-2 rounded-lg bg-gray-100 dark:bg-white/5"> <div className="p-2 bg-white/20 rounded-lg">
{getIconForType(file.type)} <Trash2 className="w-5 h-5" />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex flex-col items-start text-left">
<div className="text-sm font-semibold text-gray-900 dark:text-gray-100 capitalize">{file.type}</div> <span className="font-semibold text-base">Uninstall</span>
<div className="text-xs text-gray-500 truncate mt-0.5">{file.path}</div> <span className="text-xs opacity-90 font-normal">Remove {selectedFiles.size} selected items</span>
</div> </div>
<span className="text-sm font-mono font-medium text-gray-600 dark:text-gray-400">{formatSize(file.size)}</span> </GlassButton>
</div>
))} <div className="h-px bg-gray-100 dark:bg-white/10 my-2" />
</div>
</GlassCard> <div className="grid grid-cols-1 gap-3">
</div> <button
onClick={() => handleAction('reset')}
{/* Actions */} disabled={processing}
<div className="space-y-6"> className="w-full flex items-center gap-3 p-3 rounded-xl hover:bg-gray-50 dark:hover:bg-white/5 transition-colors border border-transparent hover:border-gray-200 dark:hover:border-white/10 group"
<GlassCard className="p-5 space-y-5"> >
<h3 className="font-semibold text-gray-900 dark:text-gray-200">Cleanup Actions</h3> <div className="p-2 rounded-lg bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400 group-hover:bg-blue-100 dark:group-hover:bg-blue-500/20 transition-colors">
<RefreshCw className="w-4 h-4" />
<GlassButton </div>
variant="danger" <div className="flex flex-col items-start text-left">
className="w-full justify-start gap-4 p-4 h-auto" <span className="font-medium text-gray-900 dark:text-gray-200 text-sm">Reset Application</span>
onClick={() => handleAction('uninstall')} <span className="text-xs text-gray-500">Delete config & data only</span>
disabled={processing} </div>
> </button>
<div className="p-2 bg-white/20 rounded-lg">
<Trash2 className="w-5 h-5" /> <button
</div> onClick={() => handleAction('cache')}
<div className="flex flex-col items-start text-left"> disabled={processing}
<span className="font-semibold text-base">Uninstall</span> className="w-full flex items-center gap-3 p-3 rounded-xl hover:bg-gray-50 dark:hover:bg-white/5 transition-colors border border-transparent hover:border-gray-200 dark:hover:border-white/10 group"
<span className="text-xs opacity-90 font-normal">Remove {selectedFiles.size} selected items</span> >
</div> <div className="p-2 rounded-lg bg-orange-50 dark:bg-orange-500/10 text-orange-600 dark:text-orange-400 group-hover:bg-orange-100 dark:group-hover:bg-orange-500/20 transition-colors">
</GlassButton> <Eraser className="w-4 h-4" />
</div>
<div className="h-px bg-gray-100 dark:bg-white/10 my-2" /> <div className="flex flex-col items-start text-left">
<span className="font-medium text-gray-900 dark:text-gray-200 text-sm">Clear Cache</span>
<div className="grid grid-cols-1 gap-3"> <span className="text-xs text-gray-500">Remove temporary files</span>
<button </div>
onClick={() => handleAction('reset')} </button>
disabled={processing} </div>
className="w-full flex items-center gap-3 p-3 rounded-xl hover:bg-gray-50 dark:hover:bg-white/5 transition-colors border border-transparent hover:border-gray-200 dark:hover:border-white/10 group" </GlassCard>
>
<div className="p-2 rounded-lg bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400 group-hover:bg-blue-100 dark:group-hover:bg-blue-500/20 transition-colors"> <div className="p-4 rounded-xl bg-yellow-50 dark:bg-yellow-500/10 border border-yellow-200 dark:border-yellow-500/20 flex gap-3">
<RefreshCw className="w-4 h-4" /> <AlertTriangle className="w-5 h-5 text-yellow-600 dark:text-yellow-500 shrink-0 mt-0.5" />
</div> <p className="text-xs text-yellow-800 dark:text-yellow-200/80 leading-relaxed font-medium">
<div className="flex flex-col items-start text-left"> Deleted files cannot be recovered. Ensure you have backups of important data before uninstalling applications.
<span className="font-medium text-gray-900 dark:text-gray-200 text-sm">Reset Application</span> </p>
<span className="text-xs text-gray-500">Delete config & data only</span> </div>
</div> </div>
</button> </div>
</div>
<button );
onClick={() => handleAction('cache')} }
disabled={processing}
className="w-full flex items-center gap-3 p-3 rounded-xl hover:bg-gray-50 dark:hover:bg-white/5 transition-colors border border-transparent hover:border-gray-200 dark:hover:border-white/10 group" function PackageIcon({ className }: { className?: string }) {
> return (
<div className="p-2 rounded-lg bg-orange-50 dark:bg-orange-500/10 text-orange-600 dark:text-orange-400 group-hover:bg-orange-100 dark:group-hover:bg-orange-500/20 transition-colors"> <svg
<Eraser className="w-4 h-4" /> xmlns="http://www.w3.org/2000/svg"
</div> viewBox="0 0 24 24"
<div className="flex flex-col items-start text-left"> fill="none"
<span className="font-medium text-gray-900 dark:text-gray-200 text-sm">Clear Cache</span> stroke="currentColor"
<span className="text-xs text-gray-500">Remove temporary files</span> strokeWidth="2"
</div> strokeLinecap="round"
</button> strokeLinejoin="round"
</div> className={className}
</GlassCard> >
<path d="m16.5 9.4-9-5.19M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
<div className="p-4 rounded-xl bg-yellow-50 dark:bg-yellow-500/10 border border-yellow-200 dark:border-yellow-500/20 flex gap-3"> <polyline points="3.27 6.96 12 12.01 20.73 6.96" />
<AlertTriangle className="w-5 h-5 text-yellow-600 dark:text-yellow-500 shrink-0 mt-0.5" /> <line x1="12" y1="22.08" x2="12" y2="12" />
<p className="text-xs text-yellow-800 dark:text-yellow-200/80 leading-relaxed font-medium"> </svg>
Deleted files cannot be recovered. Ensure you have backups of important data before uninstalling applications. )
</p> }
</div>
</div>
</div>
</div>
);
}
function PackageIcon({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="m16.5 9.4-9-5.19M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
<polyline points="3.27 6.96 12 12.01 20.73 6.96" />
<line x1="12" y1="22.08" x2="12" y2="12" />
</svg>
)
}

View file

@ -1,212 +1,212 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { Search, Package, LayoutGrid, List, ArrowUpDown } from 'lucide-react'; import { Search, Package, LayoutGrid, List, ArrowUpDown } from 'lucide-react';
import { API } from '../../api/client'; import { API } from '../../api/client';
import type { AppInfo } from '../../api/client'; import type { AppInfo } from '../../api/client';
import { GlassCard } from '../ui/GlassCard'; import { GlassCard } from '../ui/GlassCard';
import { AppDetailsView } from './AppDetails'; import { AppDetailsView } from './AppDetails';
export function AppsView() { export function AppsView() {
const [apps, setApps] = useState<AppInfo[]>([]); const [apps, setApps] = useState<AppInfo[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [selectedApp, setSelectedApp] = useState<AppInfo | null>(null); const [selectedApp, setSelectedApp] = useState<AppInfo | null>(null);
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid'); const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const [sortMode, setSortMode] = useState<'size' | 'name'>('size'); const [sortMode, setSortMode] = useState<'size' | 'name'>('size');
useEffect(() => { useEffect(() => {
loadApps(); loadApps();
}, []); }, []);
const loadApps = async () => { const loadApps = async () => {
try { try {
setLoading(true); setLoading(true);
const data = await API.getApps(); const data = await API.getApps();
// Initial load - sort logic handled in render/memo usually but let's just set raw data // Initial load - sort logic handled in render/memo usually but let's just set raw data
// Populate icons // Populate icons
const appsWithIcons = await Promise.all(data.map(async (app) => { const appsWithIcons = await Promise.all(data.map(async (app) => {
const icon = await API.getAppIcon(app.path); const icon = await API.getAppIcon(app.path);
return { ...app, icon }; return { ...app, icon };
})); }));
setApps(appsWithIcons); setApps(appsWithIcons);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const formatSize = (bytes: number) => { const formatSize = (bytes: number) => {
const units = ['B', 'KB', 'MB', 'GB']; const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes; let size = bytes;
let unitIndex = 0; let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) { while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024; size /= 1024;
unitIndex++; unitIndex++;
} }
return `${size.toFixed(1)} ${units[unitIndex]}`; return `${size.toFixed(1)} ${units[unitIndex]}`;
}; };
const sortedApps = [...apps].filter(app => const sortedApps = [...apps].filter(app =>
app.name.toLowerCase().includes(search.toLowerCase()) app.name.toLowerCase().includes(search.toLowerCase())
).sort((a, b) => { ).sort((a, b) => {
if (sortMode === 'size') { if (sortMode === 'size') {
return b.size - a.size; return b.size - a.size;
} else { } else {
return a.name.localeCompare(b.name); return a.name.localeCompare(b.name);
} }
}); });
if (selectedApp) { if (selectedApp) {
return ( return (
<AppDetailsView <AppDetailsView
app={selectedApp} app={selectedApp}
onBack={() => setSelectedApp(null)} onBack={() => setSelectedApp(null)}
onUninstall={() => { onUninstall={() => {
loadApps(); loadApps();
setSelectedApp(null); setSelectedApp(null);
}} }}
/> />
); );
} }
return ( return (
<div className="space-y-6 max-w-7xl mx-auto p-6 h-full flex flex-col"> <div className="space-y-6 max-w-7xl mx-auto p-6 h-full flex flex-col">
<header className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-4 shrink-0"> <header className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-4 shrink-0">
<div> <div>
<h1 className="text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-purple-400 dark:from-blue-300 dark:to-purple-300"> <h1 className="text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-purple-400 dark:from-blue-300 dark:to-purple-300">
App Uninstaller App Uninstaller
</h1> </h1>
<p className="text-gray-500 dark:text-gray-400 mt-1">Scan and remove applications completely</p> <p className="text-gray-500 dark:text-gray-400 mt-1">Scan and remove applications completely</p>
</div> </div>
<div className="flex items-center gap-3 w-full md:w-auto"> <div className="flex items-center gap-3 w-full md:w-auto">
{/* View Toggle */} {/* View Toggle */}
<div className="flex items-center bg-gray-100 dark:bg-white/5 rounded-lg p-1 border border-gray-200 dark:border-white/10"> <div className="flex items-center bg-gray-100 dark:bg-white/5 rounded-lg p-1 border border-gray-200 dark:border-white/10">
<button <button
onClick={() => setViewMode('grid')} onClick={() => setViewMode('grid')}
className={`p-2 rounded-md transition-all ${viewMode === 'grid' ? 'bg-white dark:bg-white/10 shadow-sm text-blue-500' : 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'}`} className={`p-2 rounded-md transition-all ${viewMode === 'grid' ? 'bg-white dark:bg-white/10 shadow-sm text-blue-500' : 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'}`}
> >
<LayoutGrid className="w-4 h-4" /> <LayoutGrid className="w-4 h-4" />
</button> </button>
<button <button
onClick={() => setViewMode('list')} onClick={() => setViewMode('list')}
className={`p-2 rounded-md transition-all ${viewMode === 'list' ? 'bg-white dark:bg-white/10 shadow-sm text-blue-500' : 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'}`} className={`p-2 rounded-md transition-all ${viewMode === 'list' ? 'bg-white dark:bg-white/10 shadow-sm text-blue-500' : 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'}`}
> >
<List className="w-4 h-4" /> <List className="w-4 h-4" />
</button> </button>
</div> </div>
{/* Sort Dropdown (Simple Toggle for now) */} {/* Sort Dropdown (Simple Toggle for now) */}
<button <button
onClick={() => setSortMode(prev => prev === 'size' ? 'name' : 'size')} onClick={() => setSortMode(prev => prev === 'size' ? 'name' : 'size')}
className="flex items-center gap-2 px-3 py-2 bg-gray-100 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-lg text-sm text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-white/10 transition-colors" className="flex items-center gap-2 px-3 py-2 bg-gray-100 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-lg text-sm text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-white/10 transition-colors"
> >
<ArrowUpDown className="w-4 h-4" /> <ArrowUpDown className="w-4 h-4" />
<span>Sort by {sortMode === 'size' ? 'Size' : 'Name'}</span> <span>Sort by {sortMode === 'size' ? 'Size' : 'Name'}</span>
</button> </button>
{/* Search */} {/* Search */}
<div className="relative w-full md:w-64"> <div className="relative w-full md:w-64">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input <input
type="text" type="text"
placeholder="Search apps..." placeholder="Search apps..."
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
className="w-full bg-white/50 dark:bg-black/20 border border-gray-200 dark:border-white/10 rounded-xl pl-10 pr-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500/50 transition-all text-gray-900 dark:text-white placeholder-gray-400" className="w-full bg-white/50 dark:bg-black/20 border border-gray-200 dark:border-white/10 rounded-xl pl-10 pr-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500/50 transition-all text-gray-900 dark:text-white placeholder-gray-400"
/> />
</div> </div>
</div> </div>
</header> </header>
{loading ? ( {loading ? (
<div className="flex flex-col items-center justify-center py-32 text-gray-400"> <div className="flex flex-col items-center justify-center py-32 text-gray-400">
<div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin mb-4" /> <div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin mb-4" />
<p className="font-medium">Scanning applications...</p> <p className="font-medium">Scanning applications...</p>
</div> </div>
) : ( ) : (
<div className="flex-1 overflow-y-auto pr-2 -mr-2"> <div className="flex-1 overflow-y-auto pr-2 -mr-2">
{viewMode === 'grid' ? ( {viewMode === 'grid' ? (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4 pb-10"> <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4 pb-10">
<AnimatePresence> <AnimatePresence>
{sortedApps.map((app) => ( {sortedApps.map((app) => (
<motion.div <motion.div
key={app.bundleID} key={app.path}
initial={{ opacity: 0, scale: 0.95 }} initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }} exit={{ opacity: 0, scale: 0.95 }}
layout layout
> >
<GlassCard <GlassCard
className="group hover:bg-white/80 dark:hover:bg-white/5 transition-all duration-300 cursor-pointer h-full border border-transparent hover:border-blue-500/20 hover:shadow-xl hover:shadow-blue-500/5 hover:-translate-y-1" className="group hover:bg-white/80 dark:hover:bg-white/5 transition-all duration-300 cursor-pointer h-full border border-transparent hover:border-blue-500/20 hover:shadow-xl hover:shadow-blue-500/5 hover:-translate-y-1"
onClick={() => setSelectedApp(app)} onClick={() => setSelectedApp(app)}
> >
<div className="flex flex-col items-center p-5 text-center h-full"> <div className="flex flex-col items-center p-5 text-center h-full">
<div className="w-16 h-16 mb-4 relative"> <div className="w-16 h-16 mb-4 relative">
{app.icon ? ( {app.icon ? (
<img src={app.icon} alt={app.name} className="w-full h-full object-contain drop-shadow-md" /> <img src={app.icon} alt={app.name} className="w-full h-full object-contain drop-shadow-md" />
) : ( ) : (
<div className="w-full h-full rounded-xl bg-gradient-to-br from-blue-500/10 to-purple-500/10 flex items-center justify-center text-blue-400"> <div className="w-full h-full rounded-xl bg-gradient-to-br from-blue-500/10 to-purple-500/10 flex items-center justify-center text-blue-400">
<Package className="w-8 h-8 opacity-50" /> <Package className="w-8 h-8 opacity-50" />
</div> </div>
)} )}
</div> </div>
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-1 line-clamp-1 w-full text-sm md:text-base" title={app.name}> <h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-1 line-clamp-1 w-full text-sm md:text-base" title={app.name}>
{app.name} {app.name}
</h3> </h3>
<p className="text-[10px] md:text-xs text-gray-500 dark:text-gray-400 font-mono mb-3 bg-gray-100 dark:bg-white/5 px-2 py-0.5 rounded-full"> <p className="text-[10px] md:text-xs text-gray-500 dark:text-gray-400 font-mono mb-3 bg-gray-100 dark:bg-white/5 px-2 py-0.5 rounded-full">
{formatSize(app.size)} {formatSize(app.size)}
</p> </p>
</div> </div>
</GlassCard> </GlassCard>
</motion.div> </motion.div>
))} ))}
</AnimatePresence> </AnimatePresence>
</div> </div>
) : ( ) : (
// List View Logic // List View Logic
<div className="flex flex-col gap-2 pb-10"> <div className="flex flex-col gap-2 pb-10">
{sortedApps.map((app) => ( {sortedApps.map((app) => (
<motion.div <motion.div
key={app.bundleID} key={app.path}
initial={{ opacity: 0, y: 10 }} initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="group flex items-center gap-4 p-3 rounded-xl hover:bg-white/80 dark:hover:bg-white/5 border border-transparent hover:border-gray-200 dark:hover:border-white/10 transition-all cursor-pointer" className="group flex items-center gap-4 p-3 rounded-xl hover:bg-white/80 dark:hover:bg-white/5 border border-transparent hover:border-gray-200 dark:hover:border-white/10 transition-all cursor-pointer"
onClick={() => setSelectedApp(app)} onClick={() => setSelectedApp(app)}
> >
<div className="w-10 h-10 shrink-0"> <div className="w-10 h-10 shrink-0">
{app.icon ? ( {app.icon ? (
<img src={app.icon} alt={app.name} className="w-full h-full object-contain" /> <img src={app.icon} alt={app.name} className="w-full h-full object-contain" />
) : ( ) : (
<div className="w-full h-full rounded-lg bg-blue-500/10 flex items-center justify-center"> <div className="w-full h-full rounded-lg bg-blue-500/10 flex items-center justify-center">
<Package className="w-5 h-5 text-blue-500" /> <Package className="w-5 h-5 text-blue-500" />
</div> </div>
)} )}
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h3 className="font-semibold text-gray-900 dark:text-gray-100 truncate text-sm md:text-base">{app.name}</h3> <h3 className="font-semibold text-gray-900 dark:text-gray-100 truncate text-sm md:text-base">{app.name}</h3>
<p className="text-[11px] md:text-xs text-gray-500 dark:text-gray-400 truncate">{app.path}</p> <p className="text-[11px] md:text-xs text-gray-500 dark:text-gray-400 truncate">{app.path}</p>
</div> </div>
<div className="shrink-0 text-xs md:text-sm font-mono text-gray-600 dark:text-gray-400"> <div className="shrink-0 text-xs md:text-sm font-mono text-gray-600 dark:text-gray-400">
{formatSize(app.size)} {formatSize(app.size)}
</div> </div>
</motion.div> </motion.div>
))} ))}
</div> </div>
)} )}
{sortedApps.length === 0 && ( {sortedApps.length === 0 && (
<div className="text-center py-20 text-gray-500"> <div className="text-center py-20 text-gray-500">
No applications found matching "{search}" No applications found matching "{search}"
</div> </div>
)} )}
</div> </div>
)} )}
</div> </div>
); );
} }

View file

@ -1,4 +1,4 @@
export { GlassCard } from './GlassCard'; export { GlassCard } from './GlassCard';
export { GlassButton } from './GlassButton'; export { GlassButton } from './GlassButton';
export { ToastProvider, useToast } from './Toast'; export { ToastProvider, useToast } from './Toast';
export { Tooltip } from './Tooltip'; export { Tooltip } from './Tooltip';

486
src/index.css Normal file → Executable file
View file

@ -1,244 +1,244 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* ============================================ /* ============================================
LIQUID GLASS DESIGN SYSTEM (macOS 26 Tahoe) LIQUID GLASS DESIGN SYSTEM (macOS 26 Tahoe)
============================================ */ ============================================ */
:root { :root {
/* Glass Effect Variables */ /* Glass Effect Variables */
--glass-bg: rgba(255, 255, 255, 0.72); --glass-bg: rgba(255, 255, 255, 0.72);
--glass-bg-light: rgba(255, 255, 255, 0.85); --glass-bg-light: rgba(255, 255, 255, 0.85);
--glass-bg-heavy: rgba(255, 255, 255, 0.92); --glass-bg-heavy: rgba(255, 255, 255, 0.92);
--glass-blur: 40px; --glass-blur: 40px;
--glass-blur-light: 20px; --glass-blur-light: 20px;
--glass-border: rgba(255, 255, 255, 0.18); --glass-border: rgba(255, 255, 255, 0.18);
--glass-border-strong: rgba(255, 255, 255, 0.35); --glass-border-strong: rgba(255, 255, 255, 0.35);
--glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.08); --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
--glass-shadow-elevated: 0 16px 48px rgba(0, 0, 0, 0.12); --glass-shadow-elevated: 0 16px 48px rgba(0, 0, 0, 0.12);
--glass-inner-glow: inset 0 1px 0 rgba(255, 255, 255, 0.5); --glass-inner-glow: inset 0 1px 0 rgba(255, 255, 255, 0.5);
/* Radius System */ /* Radius System */
--radius-xs: 8px; --radius-xs: 8px;
--radius-sm: 12px; --radius-sm: 12px;
--radius-md: 16px; --radius-md: 16px;
--radius-lg: 20px; --radius-lg: 20px;
--radius-xl: 24px; --radius-xl: 24px;
--radius-full: 9999px; --radius-full: 9999px;
/* Timing Functions */ /* Timing Functions */
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--ease-smooth: cubic-bezier(0.4, 0, 0.2, 1); --ease-smooth: cubic-bezier(0.4, 0, 0.2, 1);
--ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55); --ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
/* Base Colors */ /* Base Colors */
--color-surface: #f5f5f7; --color-surface: #f5f5f7;
--color-text: #1d1d1f; --color-text: #1d1d1f;
--color-text-secondary: rgba(0, 0, 0, 0.55); --color-text-secondary: rgba(0, 0, 0, 0.55);
--color-accent: #007AFF; --color-accent: #007AFF;
--color-danger: #FF3B30; --color-danger: #FF3B30;
--color-success: #34C759; --color-success: #34C759;
background-color: var(--color-surface); background-color: var(--color-surface);
color: var(--color-text); color: var(--color-text);
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root {
--glass-bg: rgba(30, 30, 30, 0.72); --glass-bg: rgba(30, 30, 30, 0.72);
--glass-bg-light: rgba(40, 40, 40, 0.85); --glass-bg-light: rgba(40, 40, 40, 0.85);
--glass-bg-heavy: rgba(20, 20, 20, 0.92); --glass-bg-heavy: rgba(20, 20, 20, 0.92);
--glass-border: rgba(255, 255, 255, 0.1); --glass-border: rgba(255, 255, 255, 0.1);
--glass-border-strong: rgba(255, 255, 255, 0.15); --glass-border-strong: rgba(255, 255, 255, 0.15);
--glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
--glass-shadow-elevated: 0 16px 48px rgba(0, 0, 0, 0.5); --glass-shadow-elevated: 0 16px 48px rgba(0, 0, 0, 0.5);
--glass-inner-glow: inset 0 1px 0 rgba(255, 255, 255, 0.1); --glass-inner-glow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
--color-surface: #000000; --color-surface: #000000;
--color-text: #f5f5f7; --color-text: #f5f5f7;
--color-text-secondary: rgba(255, 255, 255, 0.55); --color-text-secondary: rgba(255, 255, 255, 0.55);
} }
} }
.dark { .dark {
--glass-bg: rgba(20, 20, 20, 0.7); --glass-bg: rgba(20, 20, 20, 0.7);
--glass-bg-light: rgba(30, 30, 30, 0.8); --glass-bg-light: rgba(30, 30, 30, 0.8);
--glass-bg-heavy: rgba(10, 10, 10, 0.9); --glass-bg-heavy: rgba(10, 10, 10, 0.9);
--glass-border: rgba(255, 255, 255, 0.08); --glass-border: rgba(255, 255, 255, 0.08);
--glass-border-strong: rgba(255, 255, 255, 0.12); --glass-border-strong: rgba(255, 255, 255, 0.12);
--glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
--glass-shadow-elevated: 0 16px 48px rgba(0, 0, 0, 0.6); --glass-shadow-elevated: 0 16px 48px rgba(0, 0, 0, 0.6);
--glass-inner-glow: inset 0 1px 0 rgba(255, 255, 255, 0.05); --glass-inner-glow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
--color-surface: #000000; --color-surface: #000000;
--color-text: #f5f5f7; --color-text: #f5f5f7;
--color-text-secondary: rgba(255, 255, 255, 0.55); --color-text-secondary: rgba(255, 255, 255, 0.55);
} }
body { body {
margin: 0; margin: 0;
overflow-x: hidden; overflow-x: hidden;
user-select: none; user-select: none;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
button { button {
cursor: default; cursor: default;
} }
/* ============================================ /* ============================================
GLASS UTILITY CLASSES GLASS UTILITY CLASSES
============================================ */ ============================================ */
.liquid-glass { .liquid-glass {
background: var(--glass-bg); background: var(--glass-bg);
backdrop-filter: blur(var(--glass-blur)); backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid var(--glass-border); border: 1px solid var(--glass-border);
box-shadow: var(--glass-shadow), var(--glass-inner-glow); box-shadow: var(--glass-shadow), var(--glass-inner-glow);
} }
.liquid-glass-light { .liquid-glass-light {
background: var(--glass-bg-light); background: var(--glass-bg-light);
backdrop-filter: blur(var(--glass-blur-light)); backdrop-filter: blur(var(--glass-blur-light));
-webkit-backdrop-filter: blur(var(--glass-blur-light)); -webkit-backdrop-filter: blur(var(--glass-blur-light));
border: 1px solid var(--glass-border); border: 1px solid var(--glass-border);
box-shadow: var(--glass-shadow); box-shadow: var(--glass-shadow);
} }
.liquid-glass-heavy { .liquid-glass-heavy {
background: var(--glass-bg-heavy); background: var(--glass-bg-heavy);
backdrop-filter: blur(var(--glass-blur)); backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid var(--glass-border-strong); border: 1px solid var(--glass-border-strong);
box-shadow: var(--glass-shadow-elevated), var(--glass-inner-glow); box-shadow: var(--glass-shadow-elevated), var(--glass-inner-glow);
} }
.glass-hover { .glass-hover {
transition: transform 0.2s var(--ease-spring), box-shadow 0.2s var(--ease-smooth); transition: transform 0.2s var(--ease-spring), box-shadow 0.2s var(--ease-smooth);
} }
.glass-hover:hover { .glass-hover:hover {
transform: translateY(-2px) scale(1.01); transform: translateY(-2px) scale(1.01);
box-shadow: var(--glass-shadow-elevated), var(--glass-inner-glow); box-shadow: var(--glass-shadow-elevated), var(--glass-inner-glow);
} }
.glass-press { .glass-press {
transition: transform 0.1s var(--ease-smooth); transition: transform 0.1s var(--ease-smooth);
} }
.glass-press:active { .glass-press:active {
transform: scale(0.97); transform: scale(0.97);
} }
/* ============================================ /* ============================================
ANIMATION KEYFRAMES ANIMATION KEYFRAMES
============================================ */ ============================================ */
@keyframes fadeIn { @keyframes fadeIn {
from { from {
opacity: 0; opacity: 0;
transform: translateY(8px) scale(0.98); transform: translateY(8px) scale(0.98);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0) scale(1); transform: translateY(0) scale(1);
} }
} }
@keyframes slideUp { @keyframes slideUp {
from { from {
opacity: 0; opacity: 0;
transform: translateY(24px); transform: translateY(24px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
} }
} }
@keyframes scaleIn { @keyframes scaleIn {
from { from {
opacity: 0; opacity: 0;
transform: scale(0.95); transform: scale(0.95);
} }
to { to {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
} }
} }
@keyframes shimmer { @keyframes shimmer {
0% { 0% {
background-position: -200% 0; background-position: -200% 0;
} }
100% { 100% {
background-position: 200% 0; background-position: 200% 0;
} }
} }
@keyframes pulse-glow { @keyframes pulse-glow {
0%, 0%,
100% { 100% {
box-shadow: var(--glass-shadow); box-shadow: var(--glass-shadow);
} }
50% { 50% {
box-shadow: var(--glass-shadow-elevated), 0 0 20px rgba(0, 122, 255, 0.15); box-shadow: var(--glass-shadow-elevated), 0 0 20px rgba(0, 122, 255, 0.15);
} }
} }
/* Animation Utility Classes */ /* Animation Utility Classes */
.animate-fade-in { .animate-fade-in {
animation: fadeIn 0.4s var(--ease-spring) forwards; animation: fadeIn 0.4s var(--ease-spring) forwards;
} }
.animate-slide-up { .animate-slide-up {
animation: slideUp 0.5s var(--ease-spring) forwards; animation: slideUp 0.5s var(--ease-spring) forwards;
} }
.animate-scale-in { .animate-scale-in {
animation: scaleIn 0.3s var(--ease-spring) forwards; animation: scaleIn 0.3s var(--ease-spring) forwards;
} }
/* Stagger children animations */ /* Stagger children animations */
.stagger-children>* { .stagger-children>* {
animation: fadeIn 0.4s var(--ease-spring) forwards; animation: fadeIn 0.4s var(--ease-spring) forwards;
opacity: 0; opacity: 0;
} }
.stagger-children>*:nth-child(1) { .stagger-children>*:nth-child(1) {
animation-delay: 0s; animation-delay: 0s;
} }
.stagger-children>*:nth-child(2) { .stagger-children>*:nth-child(2) {
animation-delay: 0.05s; animation-delay: 0.05s;
} }
.stagger-children>*:nth-child(3) { .stagger-children>*:nth-child(3) {
animation-delay: 0.1s; animation-delay: 0.1s;
} }
.stagger-children>*:nth-child(4) { .stagger-children>*:nth-child(4) {
animation-delay: 0.15s; animation-delay: 0.15s;
} }
.stagger-children>*:nth-child(5) { .stagger-children>*:nth-child(5) {
animation-delay: 0.2s; animation-delay: 0.2s;
} }
.stagger-children>*:nth-child(6) { .stagger-children>*:nth-child(6) {
animation-delay: 0.25s; animation-delay: 0.25s;
} }

0
src/main.tsx Normal file → Executable file
View file

0
src/vite-env.d.ts vendored Normal file → Executable file
View file

View file

@ -1,55 +0,0 @@
# Start-Dev.ps1 - Windows equivalent of start-go.sh
Write-Host "Starting Antigravity (Windows Mode)..." -ForegroundColor Green
# 1. Kill existing backend on port 36969 if running
$port = 36969
$process = Get-NetTCPConnection -LocalPort $port -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess -Unique
if ($process) {
Write-Host "Killing existing backend process (PID: $process)..." -ForegroundColor Yellow
Stop-Process -Id $process -Force -ErrorAction SilentlyContinue
}
# 2. Check for Go
if (-not (Get-Command "go" -ErrorAction SilentlyContinue)) {
Write-Host "Go is not installed or not in PATH." -ForegroundColor Red
exit 1
}
# 3. Check for pnpm or fallback
$pkgManager = "pnpm"
if (-not (Get-Command "pnpm" -ErrorAction SilentlyContinue)) {
if (Get-Command "npm" -ErrorAction SilentlyContinue) {
$pkgManager = "npm"
}
else {
Write-Host "pnpm/npm not found." -ForegroundColor Red
exit 1
}
}
# 4. Start Backend in background
Write-Host "Starting Go Backend..." -ForegroundColor Cyan
$env:APP_ENV = "development"
$backendJob = Start-Process -FilePath "go" -ArgumentList "run backend/main.go" -NoNewWindow -PassThru
Write-Host "Backend started (Simple PID: $($backendJob.Id))" -ForegroundColor Gray
# 5. Start Frontend
Write-Host "Checking dependencies..." -ForegroundColor Cyan
if (-not (Test-Path "node_modules\.bin\vite.ps1") -and -not (Test-Path "node_modules\.bin\vite.cmd")) {
Write-Host "Dependencies missing. Running install..." -ForegroundColor Yellow
if ($pkgManager -eq "pnpm") {
pnpm install
}
else {
npm install
}
}
Write-Host "Starting Frontend ($pkgManager run dev)..." -ForegroundColor Cyan
if ($pkgManager -eq "pnpm") {
pnpm run dev
}
else {
npm run dev
}

0
start-dev.sh Normal file → Executable file
View file

2
start-go.sh Normal file → Executable file
View file

@ -26,7 +26,7 @@ GO_PID=$!
echo "✨ Starting Frontend..." echo "✨ Starting Frontend..."
# Check for pnpm or fallback # Check for pnpm or fallback
if command -v pnpm &> /dev/null; then if command -v pnpm &> /dev/null; then
pnpm run dev:electron pnpm run dev
else else
# Fallback if pnpm is also missing from PATH but bun is there # Fallback if pnpm is also missing from PATH but bun is there
bun run dev bun run dev

0
tailwind.config.js Normal file → Executable file
View file

Binary file not shown.

0
tsconfig.app.json Normal file → Executable file
View file

0
tsconfig.json Normal file → Executable file
View file

0
tsconfig.node.json Normal file → Executable file
View file

0
vite.config.ts Normal file → Executable file
View file