feat: Initial commit version 1

This commit is contained in:
vndangkhoa 2026-01-31 21:17:55 +07:00
commit 301e638900
177 changed files with 19248 additions and 0 deletions

24
.gitignore vendored Executable file
View file

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

2
.npmrc Executable file
View file

@ -0,0 +1,2 @@
node-linker=hoisted
package-import-method=clone-or-copy

68
README.md Executable file
View file

@ -0,0 +1,68 @@
# KV Clearnup (Antigravity) 🚀
A modern, high-performance system optimizer for macOS, built with **Electron**, **React**, and **Go**.
![App Screenshot](https://via.placeholder.com/800x500?text=Antigravity+Dashboard)
## Features
- **Flash Clean**: Instantly remove system caches, logs, and trash.
- **Deep Clean**: Scan for large files and heavy folders.
- **Real-time Monitoring**: Track disk usage and category sizes.
- **Universal Binary**: Runs natively on both Apple Silicon (M1/M2/M3) and Intel Macs.
- **High Performance**: Heavy lifting is handled by a compiled Go backend.
## Prerequisites
- **Node.js** (v18+)
- **Go** (v1.20+)
- **pnpm** (preferred) or npm
## Development
### 1. Install Dependencies
```bash
npm install
```
### 2. Run in Development Mode
This starts the Go backend (port 36969) and the Vite/Electron frontend concurrently.
```bash
./start-go.sh
```
*Note: Do not run `npm run dev` directly if you want the backend to work. Use the script.*
## Building for Production
To create a distributable `.dmg` file for macOS:
### 1. Build the App
```bash
npm run build:mac
```
This command will:
1. Compile the Go backend for both `amd64` and `arm64`.
2. Create a universal binary using `lipo`.
3. Build the React frontend.
4. Package the Electron app and bundle the backend.
5. Generate a universal `.dmg`.
### 2. Locate the Installer
The output file will be at:
```
release/KV Clearnup-0.0.0-universal.dmg
```
## Running the App
1. **Mount the DMG**: Double-click the `.dmg` file in the `release` folder.
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.
## Architecture
- **Frontend**: React, TypeScript, TailwindCSS, Framer Motion.
- **Main Process**: Electron (TypeScript).
- **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

View file

@ -0,0 +1,217 @@
package apps
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
)
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
func ScanApps() ([]AppInfo, error) {
// Scan /Applications and ~/Applications
home, _ := os.UserHomeDir()
dirs := []string{
"/Applications",
filepath.Join(home, "Applications"),
}
var apps []AppInfo
var mu sync.Mutex
var wg sync.WaitGroup
for _, dir := range dirs {
entries, err := os.ReadDir(dir)
if err != nil {
continue
}
for _, entry := range entries {
if strings.HasSuffix(entry.Name(), ".app") {
path := filepath.Join(dir, entry.Name())
wg.Add(1)
go func(p, name string) {
defer wg.Done()
// Get Bundle ID
bid := getBundleID(p)
if bid == "" {
return // Skip if no bundle ID (system util or broken)
}
// Get Size (fast estimate)
// using du -s -k
size := getDirSize(p)
mu.Lock()
apps = append(apps, AppInfo{
Name: strings.TrimSuffix(name, ".app"),
Path: p,
BundleID: bid,
Size: size,
})
mu.Unlock()
}(path, entry.Name())
}
}
}
wg.Wait()
return apps, nil
}
// GetAppDetails finds all associated files for a given app path
func GetAppDetails(appPath string) (*AppDetails, error) {
bid := getBundleID(appPath)
if bid == "" {
return nil, fmt.Errorf("could not determine bundle ID")
}
appSize := getDirSize(appPath)
details := &AppDetails{
AppInfo: AppInfo{
Name: filepath.Base(appPath), // simplified
Path: appPath,
BundleID: bid,
Size: appSize,
},
TotalSize: appSize,
}
home, _ := os.UserHomeDir()
library := filepath.Join(home, "Library")
// Common locations to search for Bundle ID
// Name-based search fallback is risky, staying strict to Bundle ID for now
locations := map[string]string{
"Application Support": filepath.Join(library, "Application Support"),
"Caches": filepath.Join(library, "Caches"),
"Preferences": filepath.Join(library, "Preferences"),
"Saved Application State": filepath.Join(library, "Saved Application State"),
"Logs": filepath.Join(library, "Logs"),
"Cookies": filepath.Join(library, "Cookies"),
"Containers": filepath.Join(library, "Containers"), // Sandboxed data
}
for locName, locPath := range locations {
// 1. Direct match: path/BundleID
target := filepath.Join(locPath, bid)
if exists(target) {
size := getDirSize(target)
details.Associated = append(details.Associated, AssociatedFile{
Path: target,
Type: getType(locName),
Size: size,
})
details.TotalSize += size
}
// 2. Preferences often use plist extension: path/BundleID.plist
if locName == "Preferences" {
plistPath := filepath.Join(locPath, bid+".plist")
if exists(plistPath) {
size := getFileSize(plistPath)
details.Associated = append(details.Associated, AssociatedFile{
Path: plistPath,
Type: "config",
Size: size,
})
details.TotalSize += size
}
}
}
return details, nil
}
// DeleteFiles removes the requested paths
func DeleteFiles(paths []string) error {
for _, p := range paths {
// Basic safety check: don't delete root or critical system paths
// Real implementation needs robust safeguards
if p == "/" || p == "/Applications" || p == "/System" || p == "/Library" {
continue // Skip dangerous paths
}
if err := os.RemoveAll(p); err != nil {
return err
}
}
return nil
}
// Helpers
func getBundleID(path string) string {
// mdls -name kMDItemCFBundleIdentifier -r /path/to/app
cmd := exec.Command("mdls", "-name", "kMDItemCFBundleIdentifier", "-r", path)
out, err := cmd.Output()
if err != nil {
return ""
}
res := strings.TrimSpace(string(out))
if res == "(null)" {
return ""
}
return res
}
func getDirSize(path string) int64 {
cmd := exec.Command("du", "-s", "-k", path)
out, err := cmd.Output()
if err != nil {
return 0
}
parts := strings.Fields(string(out))
if len(parts) > 0 {
var s int64
fmt.Sscanf(parts[0], "%d", &s)
return s * 1024
}
return 0
}
func getFileSize(path string) int64 {
info, err := os.Stat(path)
if err != nil {
return 0
}
return info.Size()
}
func exists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func getType(locName string) string {
switch locName {
case "Caches":
return "cache"
case "Preferences", "Cookies":
return "config"
case "Logs":
return "log"
default:
return "data"
}
}

View file

@ -0,0 +1,27 @@
package cleaner
import (
"fmt"
"os"
)
// PurgePath deletes a file or directory permanently
func PurgePath(path string) error {
// Safety check: Don't delete root or critical paths
if path == "/" || path == "" {
return fmt.Errorf("cannot delete root")
}
// Check if file exists
if _, err := os.Stat(path); os.IsNotExist(err) {
return fmt.Errorf("path does not exist")
}
// Perform deletion
err := os.RemoveAll(path)
if err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,437 @@
package scanner
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strconv"
"strings"
)
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
func GetDiskUsage() (*DiskUsage, error) {
cmd := exec.Command("diskutil", "info", "/")
out, err := cmd.Output()
if err != nil {
return nil, err
}
lines := strings.Split(string(out), "\n")
var containerTotal, containerFree int64
for _, line := range lines {
line = strings.TrimSpace(line)
// Parse "Container Total Space:" line
if strings.HasPrefix(line, "Container Total Space:") {
// Format: "Container Total Space: 245.1 GB (245107195904 Bytes)"
if start := strings.Index(line, "("); start != -1 {
if end := strings.Index(line[start:], " Bytes)"); end != -1 {
bytesStr := line[start+1 : start+end]
containerTotal, _ = strconv.ParseInt(bytesStr, 10, 64)
}
}
}
// Parse "Container Free Space:" line
if strings.HasPrefix(line, "Container Free Space:") {
if start := strings.Index(line, "("); start != -1 {
if end := strings.Index(line[start:], " Bytes)"); end != -1 {
bytesStr := line[start+1 : start+end]
containerFree, _ = strconv.ParseInt(bytesStr, 10, 64)
}
}
}
}
// Calculate used space
containerUsed := containerTotal - containerFree
toGB := func(bytes int64) string {
gb := float64(bytes) / 1024 / 1024 / 1024
return fmt.Sprintf("%.2f", gb)
}
return &DiskUsage{
TotalGB: toGB(containerTotal),
UsedGB: toGB(containerUsed),
FreeGB: toGB(containerFree),
}, nil
}
// 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
func FindHeavyFolders(root string) ([]ScanResult, error) {
// du -k -d 2 <root> | sort -nr | head -n 50
cmd := exec.Command("bash", "-c", fmt.Sprintf("du -k -d 2 \"%s\" | sort -nr | head -n 50", root))
out, err := cmd.Output()
if err != nil {
return nil, err
}
var results []ScanResult
lines := strings.Split(string(out), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
// Attempt to parse first part as size
firstSpace := strings.IndexAny(line, " \t")
if firstSpace == -1 {
continue
}
sizeStr := line[:firstSpace]
pathStr := strings.TrimSpace(line[firstSpace:])
if pathStr == root {
continue
}
sizeK, err := strconv.ParseInt(sizeStr, 10, 64)
if err == nil {
results = append(results, ScanResult{
Path: pathStr,
Size: sizeK * 1024,
IsDirectory: true,
})
}
}
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 {
// 10MB threshold
res, _ := FindLargeFiles(t, 10*1024*1024)
allResults = append(allResults, res...)
}
// Sort combined
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
}
// System targets: Caches, Logs, Application Support (selective)
targets := []string{
filepath.Join(home, "Library", "Caches"),
filepath.Join(home, "Library", "Logs"),
filepath.Join(home, "Library", "Developer", "Xcode", "DerivedData"),
}
var allResults []ScanResult
for _, t := range targets {
// 10MB threshold
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 GetDirectorySize(path string) int64 {
// du -s -k <path>
cmd := exec.Command("du", "-s", "-k", path)
out, err := cmd.Output()
if err != nil {
return 0
}
// Output is "size path"
parts := strings.Fields(string(out))
if len(parts) > 0 {
sizeK, _ := strconv.ParseInt(parts[0], 10, 64)
return sizeK * 1024 // Bytes
}
return 0
}
func GetCategorySizes() (*CategorySizes, error) {
home, err := os.UserHomeDir()
if err != nil {
return nil, err
}
// Paths to check
docPath := filepath.Join(home, "Documents")
downPath := filepath.Join(home, "Downloads")
deskPath := filepath.Join(home, "Desktop")
musicPath := filepath.Join(home, "Music")
moviesPath := filepath.Join(home, "Movies")
caches := filepath.Join(home, "Library", "Caches")
logs := filepath.Join(home, "Library", "Logs")
xcode := filepath.Join(home, "Library", "Developer", "Xcode", "DerivedData")
trash := filepath.Join(home, ".Trash")
apps := "/Applications"
photos := filepath.Join(home, "Pictures")
icloud := filepath.Join(home, "Library", "Mobile Documents")
// Run in parallel
type result struct {
name string
size int64
}
// Increased buffer for new categories
c := make(chan result)
totalChecks := 12
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)
go check("caches", caches)
go check("logs", logs)
go check("xcode", xcode)
go check("trash", trash)
go check("apps", apps)
go check("photos", photos)
go check("icloud", icloud)
sizes := &CategorySizes{}
// Get total disk usage to calculate System Data remainder using diskutil (APFS aware)
cmd := exec.Command("diskutil", "info", "/")
out, err := cmd.Output()
var totalUsed int64
if err == nil {
lines := strings.Split(string(out), "\n")
var containerTotal, containerFree int64
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "Container Total Space:") {
if start := strings.Index(line, "("); start != -1 {
if end := strings.Index(line[start:], " Bytes)"); end != -1 {
bytesStr := line[start+1 : start+end]
containerTotal, _ = strconv.ParseInt(bytesStr, 10, 64)
}
}
}
if strings.HasPrefix(line, "Container Free Space:") {
if start := strings.Index(line, "("); start != -1 {
if end := strings.Index(line[start:], " Bytes)"); end != -1 {
bytesStr := line[start+1 : start+end]
containerFree, _ = strconv.ParseInt(bytesStr, 10, 64)
}
}
}
}
totalUsed = containerTotal - containerFree // In Bytes
}
var systemSpecific int64
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 "caches", "logs", "xcode":
systemSpecific += res.size
case "trash":
sizes.Trash += res.size
case "apps":
sizes.Apps = res.size
case "photos":
sizes.Photos = res.size
case "icloud":
sizes.ICloud = res.size
}
}
// Calculate System Data
// Ideally: System = Total - (Docs + Down + Desk + Music + Movies + Apps + Photos + iCloud + Trash)
known := sizes.Documents + sizes.Downloads + sizes.Desktop + sizes.Music + sizes.Movies + sizes.Trash + sizes.Apps + sizes.Photos + sizes.ICloud
var remainder int64 = 0
if totalUsed > known {
remainder = totalUsed - known
}
if remainder > systemSpecific {
sizes.System = remainder
} else {
sizes.System = systemSpecific
}
return sizes, nil
}
type CleaningEstimates struct {
FlashEst int64 `json:"flash_est"`
DeepEst int64 `json:"deep_est"`
}
func GetCleaningEstimates() (*CleaningEstimates, error) {
home, err := os.UserHomeDir()
if err != nil {
return nil, err
}
// Paths for Flash Clean: Caches, Logs, Trash, Xcode
caches := filepath.Join(home, "Library", "Caches")
logs := filepath.Join(home, "Library", "Logs")
trash := filepath.Join(home, ".Trash")
xcode := filepath.Join(home, "Library", "Developer", "Xcode", "DerivedData")
// Paths for Deep Clean (proxy): Downloads
downloads := filepath.Join(home, "Downloads")
type result struct {
name string
size int64
}
c := make(chan result)
totalChecks := 5
check := func(name, p string) {
c <- result{name, GetDirectorySize(p)}
}
go check("caches", caches)
go check("logs", logs)
go check("trash", trash)
go check("xcode", xcode)
go check("downloads", downloads)
estimates := &CleaningEstimates{}
for i := 0; i < totalChecks; i++ {
res := <-c
switch res.name {
case "caches", "logs", "trash", "xcode":
estimates.FlashEst += res.size
case "downloads":
estimates.DeepEst += res.size
}
}
return estimates, nil
}

463
backend/main.go Normal file
View file

@ -0,0 +1,463 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"sort"
"github.com/kv/clearnup/backend/internal/apps"
"github.com/kv/clearnup/backend/internal/cleaner"
"github.com/kv/clearnup/backend/internal/scanner"
)
const Port = ":36969"
func enableCors(w *http.ResponseWriter) {
(*w).Header().Set("Access-Control-Allow-Origin", "*")
(*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)
http.HandleFunc("/api/scan/user", handleScanUser)
http.HandleFunc("/api/scan/system", handleScanSystem) // Detailed list
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/empty-trash", handleEmptyTrash)
http.HandleFunc("/api/clear-cache", handleClearCache)
http.HandleFunc("/api/clean-docker", handleCleanDocker)
http.HandleFunc("/api/system-info", handleSystemInfo)
http.HandleFunc("/api/estimates", handleCleaningEstimates)
// App Uninstaller
http.HandleFunc("/api/apps", handleScanApps)
http.HandleFunc("/api/apps/details", handleAppDetails)
http.HandleFunc("/api/apps/action", handleAppAction)
fmt.Printf("🚀 Antigravity Backend running on http://localhost%s\n", Port)
if err := http.ListenAndServe(Port, nil); err != nil {
fmt.Printf("Server failed: %s\n", err)
}
}
type ScanRequest struct {
Category string `json:"category"` // "apps", "photos", "icloud", "docs", "system"
}
func handleScanCategory(w http.ResponseWriter, r *http.Request) {
enableCors(&w)
if r.Method == "OPTIONS" {
return
}
var req ScanRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid body", http.StatusBadRequest)
return
}
home, _ := os.UserHomeDir()
var targets []string
switch req.Category {
case "apps":
targets = []string{"/Applications", filepath.Join(home, "Applications")}
case "photos":
targets = []string{filepath.Join(home, "Pictures")}
case "icloud":
targets = []string{filepath.Join(home, "Library", "Mobile Documents")}
case "docs":
targets = []string{filepath.Join(home, "Documents")}
case "downloads":
targets = []string{filepath.Join(home, "Downloads")}
case "desktop":
targets = []string{filepath.Join(home, "Desktop")}
case "music":
targets = []string{filepath.Join(home, "Music")}
case "movies":
targets = []string{filepath.Join(home, "Movies")}
case "system":
targets = []string{filepath.Join(home, "Library", "Caches"), filepath.Join(home, "Library", "Logs"), filepath.Join(home, "Library", "Developer", "Xcode", "DerivedData")}
default:
json.NewEncoder(w).Encode([]scanner.ScanResult{})
return
}
// 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
// Actually I put ScanPath in scanner.go as FindLargeFiles wrapper.
var allResults []scanner.ScanResult
for _, t := range targets {
res, _ := scanner.FindLargeFiles(t, 10*1024*1024) // 10MB
allResults = append(allResults, res...)
}
// Sort
sort.Slice(allResults, func(i, j int) bool {
return allResults[i].Size > allResults[j].Size
})
if len(allResults) > 50 {
allResults = allResults[:50]
}
json.NewEncoder(w).Encode(allResults)
}
func handleOpenSettings(w http.ResponseWriter, r *http.Request) {
enableCors(&w)
if r.Method == "OPTIONS" {
return
}
// Open Storage Settings
// macOS Ventura+: open x-apple.systempreferences:com.apple.settings.Storage
exec.Command("open", "x-apple.systempreferences:com.apple.settings.Storage").Run()
w.WriteHeader(http.StatusOK)
}
func handleDiskUsage(w http.ResponseWriter, r *http.Request) {
enableCors(&w)
if r.Method == "OPTIONS" {
return
}
usage, err := scanner.GetDiskUsage()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(usage)
}
func handleScanUser(w http.ResponseWriter, r *http.Request) {
enableCors(&w)
if r.Method == "OPTIONS" {
return
}
files, err := scanner.ScanUserDocuments()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(files)
}
func handleScanSizes(w http.ResponseWriter, r *http.Request) {
enableCors(&w)
if r.Method == "OPTIONS" {
return
}
sizes, err := scanner.GetCategorySizes()
if err != nil {
// Log but return empty
fmt.Println("Size scan error:", err)
json.NewEncoder(w).Encode(map[string]int64{})
return
}
json.NewEncoder(w).Encode(sizes)
}
func handleScanSystem(w http.ResponseWriter, r *http.Request) {
enableCors(&w)
if r.Method == "OPTIONS" {
return
}
files, err := scanner.ScanSystemData()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(files)
}
func handleDeepestScan(w http.ResponseWriter, r *http.Request) {
enableCors(&w)
if r.Method == "OPTIONS" {
return
}
// Default to Documents for now, or parse body for path
home, _ := os.UserHomeDir()
target := filepath.Join(home, "Documents")
folders, err := scanner.FindHeavyFolders(target)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(folders)
}
type PurgeRequest struct {
Path string `json:"path"`
}
func handlePurge(w http.ResponseWriter, r *http.Request) {
enableCors(&w)
if r.Method == "OPTIONS" {
return
}
var req PurgeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if err := cleaner.PurgePath(req.Path); err != nil {
http.Error(w, fmt.Sprintf("Failed to purge: %s", err), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
func handleEmptyTrash(w http.ResponseWriter, r *http.Request) {
enableCors(&w)
if r.Method == "OPTIONS" {
return
}
home, err := os.UserHomeDir()
if err != nil {
http.Error(w, "Cannot get home directory", http.StatusInternalServerError)
return
}
trashPath := filepath.Join(home, ".Trash")
// Get all items in trash and delete them
entries, err := os.ReadDir(trashPath)
if err != nil {
http.Error(w, "Cannot read trash", http.StatusInternalServerError)
return
}
for _, entry := range entries {
itemPath := filepath.Join(trashPath, entry.Name())
os.RemoveAll(itemPath)
}
json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
func handleClearCache(w http.ResponseWriter, r *http.Request) {
enableCors(&w)
if r.Method == "OPTIONS" {
return
}
home, _ := os.UserHomeDir()
cachePath := filepath.Join(home, "Library", "Caches")
// Get size before clearing
sizeBefore := scanner.GetDirectorySize(cachePath)
// Clear cache directories (keep the Caches folder itself)
entries, err := os.ReadDir(cachePath)
if err != nil {
http.Error(w, "Cannot read cache directory", http.StatusInternalServerError)
return
}
for _, entry := range entries {
itemPath := filepath.Join(cachePath, entry.Name())
os.RemoveAll(itemPath)
}
json.NewEncoder(w).Encode(map[string]int64{"cleared": sizeBefore})
}
func handleCleanDocker(w http.ResponseWriter, r *http.Request) {
enableCors(&w)
if r.Method == "OPTIONS" {
return
}
// Try to find docker executable
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
break
}
}
}
if dockerPath == "" {
json.NewEncoder(w).Encode(map[string]interface{}{
"cleared": 0,
"message": "Docker not found in PATH or common locations",
})
return
}
// Run docker system prune -af
cmd := exec.Command(dockerPath, "system", "prune", "-af")
output, err := cmd.CombinedOutput()
if err != nil {
json.NewEncoder(w).Encode(map[string]interface{}{
"cleared": 0,
"message": fmt.Sprintf("Docker cleanup failed: %s", err),
})
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"cleared": 1,
"message": string(output),
})
}
func handleSystemInfo(w http.ResponseWriter, r *http.Request) {
enableCors(&w)
if r.Method == "OPTIONS" {
return
}
// 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 {
http.Error(w, "Failed to get system info", http.StatusInternalServerError)
return
}
var profile SystemProfile
if err := json.Unmarshal(output, &profile); err != nil {
http.Error(w, "Failed to parse system info", http.StatusInternalServerError)
return
}
response := map[string]string{
"model": "Unknown",
"chip": "Unknown",
"memory": "Unknown",
"os": "Unknown",
}
if len(profile.Hardware) > 0 {
response["model"] = profile.Hardware[0].MachineName
response["chip"] = profile.Hardware[0].ChipType
response["memory"] = profile.Hardware[0].PhysicalMemory
}
if len(profile.Software) > 0 {
response["os"] = profile.Software[0].OSVersion
}
json.NewEncoder(w).Encode(response)
}
func handleCleaningEstimates(w http.ResponseWriter, r *http.Request) {
enableCors(&w)
if r.Method == "OPTIONS" {
return
}
estimates, err := scanner.GetCleaningEstimates()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(estimates)
}
// App Uninstaller Handlers
func handleScanApps(w http.ResponseWriter, r *http.Request) {
enableCors(&w)
if r.Method == "OPTIONS" {
return
}
appsList, err := apps.ScanApps()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(appsList)
}
type AppDetailsRequest struct {
Path string `json:"path"`
}
func handleAppDetails(w http.ResponseWriter, r *http.Request) {
enableCors(&w)
if r.Method == "OPTIONS" {
return
}
var req AppDetailsRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
details, err := apps.GetAppDetails(req.Path)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(details)
}
type AppActionRequest struct {
Files []string `json:"files"`
}
func handleAppAction(w http.ResponseWriter, r *http.Request) {
enableCors(&w)
if r.Method == "OPTIONS" {
return
}
var req AppActionRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if err := apps.DeleteFiles(req.Files); err != nil {
http.Error(w, fmt.Sprintf("Failed to delete files: %s", err), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]bool{"success": true})
}

BIN
build/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

467
dist-electron/main.cjs Normal file
View file

@ -0,0 +1,467 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
// electron/main.ts
var import_electron = require("electron");
var import_fs = __toESM(require("fs"), 1);
var import_path3 = __toESM(require("path"), 1);
var import_child_process3 = require("child_process");
// electron/features/scanner.ts
var import_promises = __toESM(require("fs/promises"), 1);
var import_path = __toESM(require("path"), 1);
var import_os = __toESM(require("os"), 1);
var import_child_process = require("child_process");
var import_util = __toESM(require("util"), 1);
async function scanDirectory(rootDir, maxDepth = 5) {
const results = [];
async function traverse(currentPath, depth) {
if (depth > maxDepth) return;
try {
const entries = await import_promises.default.readdir(currentPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = import_path.default.join(currentPath, entry.name);
if (entry.isDirectory()) {
if (entry.name === "node_modules" || entry.name === "vendor" || entry.name === ".venv") {
try {
const stats = await import_promises.default.stat(fullPath);
results.push({
path: fullPath,
size: 0,
// Calculating size is expensive, might do lazily or separate task
lastAccessed: stats.atime,
type: entry.name
});
continue;
} catch (e) {
console.error(`Error stat-ing ${fullPath}`, e);
}
} else if (!entry.name.startsWith(".")) {
await traverse(fullPath, depth + 1);
}
}
}
} catch (error) {
console.error(`Error scanning ${currentPath}`, error);
}
}
await traverse(rootDir, 0);
return results;
}
async function findLargeFiles(rootDir, threshold = 100 * 1024 * 1024) {
const results = [];
async function traverse(currentPath) {
try {
const stats = await import_promises.default.stat(currentPath);
if (stats.size > threshold && !stats.isDirectory()) {
results.push({ path: currentPath, size: stats.size, isDirectory: false });
return;
}
if (stats.isDirectory()) {
if (import_path.default.basename(currentPath) === "node_modules") return;
const entries = await import_promises.default.readdir(currentPath, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith(".") && entry.name !== ".Trash") continue;
await traverse(import_path.default.join(currentPath, entry.name));
}
}
} catch (e) {
}
}
await traverse(rootDir);
return results.sort((a, b) => b.size - a.size);
}
async function getDeepDiveSummary() {
const home = import_os.default.homedir();
const targets = [
import_path.default.join(home, "Downloads"),
import_path.default.join(home, "Documents"),
import_path.default.join(home, "Desktop"),
import_path.default.join(home, "Library/Application Support")
];
const results = [];
for (const t of targets) {
console.log(`Scanning ${t}...`);
const large = await findLargeFiles(t, 50 * 1024 * 1024);
console.log(`Found ${large.length} large files in ${t}`);
results.push(...large);
}
return results.slice(0, 20);
}
var execPromise = import_util.default.promisify(import_child_process.exec);
async function getDiskUsage() {
try {
const { stdout } = await execPromise("df -k /");
const lines = stdout.trim().split("\n");
if (lines.length < 2) return null;
const parts = lines[1].split(/\s+/);
const total = parseInt(parts[1]) * 1024;
const used = parseInt(parts[2]) * 1024;
const available = parseInt(parts[3]) * 1024;
return {
totalGB: (total / 1024 / 1024 / 1024).toFixed(2),
usedGB: (used / 1024 / 1024 / 1024).toFixed(2),
freeGB: (available / 1024 / 1024 / 1024).toFixed(2)
};
} catch (e) {
console.error("Error getting disk usage:", e);
return null;
}
}
async function findHeavyFolders(rootDir) {
try {
console.log(`Deepest scan on: ${rootDir}`);
const { stdout } = await execPromise(`du -k -d 2 "${rootDir}" | sort -nr | head -n 50`);
const lines = stdout.trim().split("\n");
const results = lines.map((line) => {
const trimmed = line.trim();
const firstSpace = trimmed.indexOf(" ");
const match = trimmed.match(/^(\d+)\s+(.+)$/);
if (!match) return null;
const sizeK = parseInt(match[1]);
const fullPath = match[2];
return {
path: fullPath,
size: sizeK * 1024,
// Convert KB to Bytes
isDirectory: true
};
}).filter((item) => item !== null && item.path !== rootDir);
return results;
} catch (e) {
console.error("Deepest scan failed:", e);
return [];
}
}
// electron/features/updater.ts
var import_child_process2 = require("child_process");
var import_util2 = __toESM(require("util"), 1);
var execAsync = import_util2.default.promisify(import_child_process2.exec);
async function disableAutoUpdates(password) {
const cmds = [
"sudo -S softwareupdate --schedule off",
"sudo -S defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticCheckEnabled -bool false",
"sudo -S defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticDownload -bool false",
"sudo -S defaults write /Library/Preferences/com.apple.commerce AutoUpdate -bool false"
];
try {
await execWithSudo("softwareupdate --schedule off");
return true;
} catch (error) {
console.error("Failed to disable updates", error);
return false;
}
}
async function execWithSudo(command) {
const script = `do shell script "${command}" with administrator privileges`;
return execAsync(`osascript -e '${script}'`);
}
// electron/features/cleaner.ts
var import_promises2 = __toESM(require("fs/promises"), 1);
var import_path2 = __toESM(require("path"), 1);
var import_os2 = __toESM(require("os"), 1);
async function clearCaches() {
const cacheDir = import_path2.default.join(import_os2.default.homedir(), "Library/Caches");
try {
const entries = await import_promises2.default.readdir(cacheDir);
let freedSpace = 0;
for (const entry of entries) {
const fullPath = import_path2.default.join(cacheDir, entry);
await import_promises2.default.rm(fullPath, { recursive: true, force: true });
}
return true;
} catch (error) {
console.error("Error clearing caches", error);
return false;
}
}
async function purgePath(targetPath) {
try {
await import_promises2.default.rm(targetPath, { recursive: true, force: true });
return true;
} catch (e) {
console.error(`Failed to purge ${targetPath}`, e);
return false;
}
}
async function cleanupDocker() {
try {
const { exec: exec3 } = await import("child_process");
const util3 = await import("util");
const execAsync2 = util3.promisify(exec3);
await execAsync2("docker system prune -a --volumes -f");
return true;
} catch (e) {
console.error("Failed to cleanup docker:", e);
return false;
}
}
async function cleanupTmp() {
const tmpDir = import_os2.default.tmpdir();
let success = true;
try {
const entries = await import_promises2.default.readdir(tmpDir);
for (const entry of entries) {
try {
await import_promises2.default.rm(import_path2.default.join(tmpDir, entry), { recursive: true, force: true });
} catch (e) {
console.warn(`Skipped ${entry}`);
}
}
} catch (e) {
console.error("Failed to access tmp dir:", e);
success = false;
}
return success;
}
async function cleanupXcode() {
try {
const home = import_os2.default.homedir();
const paths = [
import_path2.default.join(home, "Library/Developer/Xcode/DerivedData"),
import_path2.default.join(home, "Library/Developer/Xcode/iOS DeviceSupport"),
import_path2.default.join(home, "Library/Developer/Xcode/Archives"),
import_path2.default.join(home, "Library/Caches/com.apple.dt.Xcode")
];
for (const p of paths) {
try {
await import_promises2.default.rm(p, { recursive: true, force: true });
} catch (e) {
console.warn(`Failed to clean ${p}`, e);
}
}
return true;
} catch (e) {
console.error("Failed to cleanup Xcode:", e);
return false;
}
}
async function cleanupTurnkey() {
try {
const home = import_os2.default.homedir();
const paths = [
import_path2.default.join(home, ".npm/_cacache"),
import_path2.default.join(home, ".yarn/cache"),
import_path2.default.join(home, "Library/pnpm/store"),
// Mac default for pnpm store if not configured otherwise
import_path2.default.join(home, ".cache/yarn"),
import_path2.default.join(home, ".gradle/caches")
];
for (const p of paths) {
try {
await import_promises2.default.rm(p, { recursive: true, force: true });
} catch (e) {
console.warn(`Failed to clean ${p}`, e);
}
}
return true;
} catch (e) {
console.error("Failed to cleanup package managers:", e);
return false;
}
}
// electron/main.ts
var mainWindow = null;
var backendProcess = null;
var tray = null;
var startBackend = () => {
if (process.env.NODE_ENV === "development") {
console.log("Development mode: Backend should be running via start-go.sh");
return;
}
const backendPath = import_path3.default.join(process.resourcesPath, "backend");
console.log("Starting backend from:", backendPath);
try {
backendProcess = (0, import_child_process3.spawn)(backendPath, [], {
stdio: "inherit"
});
backendProcess.on("error", (err) => {
console.error("Failed to start backend:", err);
});
backendProcess.on("exit", (code, signal) => {
console.log(`Backend exited with code ${code} and signal ${signal}`);
});
} catch (error) {
console.error("Error spawning backend:", error);
}
};
function createTray() {
const iconPath = import_path3.default.join(__dirname, "../dist/tray/tray-iconTemplate.png");
let finalIconPath = iconPath;
if (!import_fs.default.existsSync(iconPath)) {
finalIconPath = import_path3.default.join(__dirname, "../public/tray/tray-iconTemplate.png");
}
const image = import_electron.nativeImage.createFromPath(finalIconPath);
tray = new import_electron.Tray(image.resize({ width: 16, height: 16 }));
tray.setToolTip("Antigravity Cleaner");
updateTrayMenu("Initializing...");
}
var isDockVisible = true;
function updateTrayMenu(statusText) {
if (!tray) return;
const contextMenu = import_electron.Menu.buildFromTemplate([
{ label: `Storage: ${statusText}`, enabled: false },
{ type: "separator" },
{
label: "Open Dashboard",
click: () => {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
}
},
{
label: "Free Up Storage",
click: () => {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
}
},
{ type: "separator" },
{
label: "Show Dock Icon",
type: "checkbox",
checked: isDockVisible,
click: (menuItem) => {
isDockVisible = menuItem.checked;
if (isDockVisible) {
import_electron.app.dock.show();
} else {
import_electron.app.dock.hide();
}
}
},
{ type: "separator" },
{ label: "Quit", click: () => import_electron.app.quit() }
]);
tray.setContextMenu(contextMenu);
tray.setTitle(statusText);
}
function createWindow() {
mainWindow = new import_electron.BrowserWindow({
width: 1200,
height: 800,
backgroundColor: "#FFFFFF",
// Helps prevent white flash
webPreferences: {
preload: import_path3.default.join(__dirname, "preload.cjs"),
nodeIntegration: true,
contextIsolation: true
}
});
const isDev = process.env.NODE_ENV === "development";
const port = process.env.PORT || 5173;
if (isDev) {
mainWindow.loadURL(`http://localhost:${port}`);
} else {
mainWindow.loadFile(import_path3.default.join(__dirname, "../dist/index.html"));
}
mainWindow.on("closed", () => {
mainWindow = null;
});
}
import_electron.app.whenReady().then(() => {
import_electron.ipcMain.handle("scan-directory", async (event, path4) => {
return scanDirectory(path4);
});
import_electron.ipcMain.handle("deep-dive-scan", async () => {
return getDeepDiveSummary();
});
import_electron.ipcMain.handle("get-disk-usage", async () => {
return getDiskUsage();
});
import_electron.ipcMain.handle("deepest-scan", async (event, targetPath) => {
const target = targetPath || import_path3.default.join(import_electron.app.getPath("home"), "Documents");
return findHeavyFolders(target);
});
import_electron.ipcMain.handle("disable-updates", async () => {
return disableAutoUpdates();
});
import_electron.ipcMain.handle("clean-system", async () => {
return clearCaches();
});
import_electron.ipcMain.handle("cleanup-docker", async () => {
return cleanupDocker();
});
import_electron.ipcMain.handle("cleanup-tmp", async () => {
return cleanupTmp();
});
import_electron.ipcMain.handle("cleanup-xcode", async () => {
return cleanupXcode();
});
import_electron.ipcMain.handle("cleanup-turnkey", async () => {
return cleanupTurnkey();
});
import_electron.ipcMain.handle("purge-path", async (event, targetPath) => {
return purgePath(targetPath);
});
import_electron.ipcMain.handle("update-tray-title", (event, title) => {
if (tray) {
tray.setTitle(title);
updateTrayMenu(title);
}
});
import_electron.ipcMain.handle("get-app-icon", async (event, appPath) => {
try {
const icon = await import_electron.app.getFileIcon(appPath, { size: "normal" });
return icon.toDataURL();
} catch (e) {
console.error("Failed to get icon for:", appPath, e);
return "";
return "";
}
});
import_electron.ipcMain.handle("update-tray-icon", (event, dataUrl) => {
if (tray && dataUrl) {
const image = import_electron.nativeImage.createFromDataURL(dataUrl);
tray.setImage(image.resize({ width: 22, height: 22 }));
}
});
createWindow();
createTray();
startBackend();
});
import_electron.app.on("will-quit", () => {
if (backendProcess) {
console.log("Killing backend process...");
backendProcess.kill();
backendProcess = null;
}
});
import_electron.app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
import_electron.app.quit();
}
});
import_electron.app.on("activate", () => {
if (mainWindow === null) {
createWindow();
}
});

20
dist-electron/preload.cjs Normal file
View file

@ -0,0 +1,20 @@
"use strict";
// electron/preload.ts
var import_electron = require("electron");
import_electron.contextBridge.exposeInMainWorld("electronAPI", {
scanDirectory: (path) => import_electron.ipcRenderer.invoke("scan-directory", path),
disableUpdates: () => import_electron.ipcRenderer.invoke("disable-updates"),
cleanSystem: () => import_electron.ipcRenderer.invoke("clean-system"),
purgePath: (path) => import_electron.ipcRenderer.invoke("purge-path", path),
cleanupDocker: () => import_electron.ipcRenderer.invoke("cleanup-docker"),
cleanupTmp: () => import_electron.ipcRenderer.invoke("cleanup-tmp"),
cleanupXcode: () => import_electron.ipcRenderer.invoke("cleanup-xcode"),
cleanupTurnkey: () => import_electron.ipcRenderer.invoke("cleanup-turnkey"),
deepDiveScan: () => import_electron.ipcRenderer.invoke("deep-dive-scan"),
getDiskUsage: () => import_electron.ipcRenderer.invoke("get-disk-usage"),
deepestScan: (path) => import_electron.ipcRenderer.invoke("deepest-scan", path),
updateTrayTitle: (title) => import_electron.ipcRenderer.invoke("update-tray-title", title),
getAppIcon: (path) => import_electron.ipcRenderer.invoke("get-app-icon", path),
updateTrayIcon: (dataUrl) => import_electron.ipcRenderer.invoke("update-tray-icon", dataUrl)
});

141
electron/features/cleaner.ts Executable file
View file

@ -0,0 +1,141 @@
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
// Trash Compactor: System Sanitation
export async function clearCaches() {
const cacheDir = path.join(os.homedir(), 'Library/Caches');
// Be careful! Cleaning everything here might break apps.
// We should target specific known large caches or stale ones.
// For PRD: "Empty User Caches"
try {
const entries = await fs.readdir(cacheDir);
let freedSpace = 0;
for (const entry of entries) {
const fullPath = path.join(cacheDir, entry);
// We might want to just delete the contents, not the folder itself, or assume app recreates it.
// Safer: delete recursively.
await fs.rm(fullPath, { recursive: true, force: true });
}
return true;
} catch (error) {
console.error('Error clearing caches', error);
return false;
}
}
export async function purgePath(targetPath: string) {
try {
await fs.rm(targetPath, { recursive: true, force: true });
return true;
} catch (e) {
console.error(`Failed to purge ${targetPath}`, e);
return false;
}
}
export async function emptyTrash() {
// Uses apple script to force empty trash to avoid "in use" errors if possible,
// or `rm -rf ~/.Trash/*` (dangerous!).
// Safe way: osascript
try {
const { exec } = await import('child_process');
exec(`osascript -e 'tell application "Finder" to empty trash'`);
return true;
} catch (e) {
return false;
}
}
export async function cleanupDocker() {
try {
const { exec } = await import('child_process');
const util = await import('util');
const execAsync = util.promisify(exec);
// Prune everything: stopped containers, all images (dangling + unused), volumes, networks
await execAsync('docker system prune -a --volumes -f');
return true;
} catch (e) {
console.error('Failed to cleanup docker:', e);
return false;
}
}
export async function cleanupTmp() {
const tmpDir = os.tmpdir();
let success = true;
try {
const entries = await fs.readdir(tmpDir);
for (const entry of entries) {
try {
// Be careful not to delete hidden system files if possible, but user asked for "complete"
await fs.rm(path.join(tmpDir, entry), { recursive: true, force: true });
} catch (e) {
// Ignore individual file errors (locked files)
console.warn(`Skipped ${entry}`);
}
}
} catch (e) {
console.error('Failed to access tmp dir:', e);
success = false;
}
return success;
}
export async function cleanupXcode() {
try {
const home = os.homedir();
// Remove DerivedData and iOS DeviceSupport (Aggressive!)
const paths = [
path.join(home, 'Library/Developer/Xcode/DerivedData'),
path.join(home, 'Library/Developer/Xcode/iOS DeviceSupport'),
path.join(home, 'Library/Developer/Xcode/Archives'),
path.join(home, 'Library/Caches/com.apple.dt.Xcode')
];
for (const p of paths) {
try {
await fs.rm(p, { recursive: true, force: true });
} catch (e) {
console.warn(`Failed to clean ${p}`, e);
}
}
return true;
} catch (e) {
console.error('Failed to cleanup Xcode:', e);
return false;
}
}
export async function cleanupTurnkey() {
try {
const home = os.homedir();
// Clean package manager caches
const paths = [
path.join(home, '.npm/_cacache'),
path.join(home, '.yarn/cache'),
path.join(home, 'Library/pnpm/store'), // Mac default for pnpm store if not configured otherwise
path.join(home, '.cache/yarn'),
path.join(home, '.gradle/caches')
];
for (const p of paths) {
try {
await fs.rm(p, { recursive: true, force: true });
} catch (e) {
console.warn(`Failed to clean ${p}`, e);
}
}
return true;
} catch (e) {
console.error('Failed to cleanup package managers:', e);
return false;
}
}

45
electron/features/enforcer.ts Executable file
View file

@ -0,0 +1,45 @@
import fs from 'fs';
import path from 'path';
import { dialog, app } from 'electron';
// The pnpm Force
// Watches for package-lock.json or yarn.lock and warns the user.
export function startEnforcement(projectPath: string) {
// We can use fs.watch, but strictly we might want to watch the specific directory
// where the user is working.
// For this app, maybe we watch the projects user adds?
// Or do we implement a global watcher? The PRD implies "actively monitor".
// Monitoring the entire filesystem is expensive.
// We'll assume we monitor specific "Active Projects" added to the app.
// Implementation note: This function would be called for each watched project.
const watcher = fs.watch(projectPath, (eventType, filename) => {
if (filename === 'package-lock.json' || filename === 'yarn.lock') {
const lockFile = path.join(projectPath, filename);
if (fs.existsSync(lockFile)) {
handleViolation(lockFile);
}
}
});
return watcher;
}
function handleViolation(lockFile: string) {
dialog.showMessageBox({
type: 'warning',
title: 'The pnpm Force',
message: `Illegal lockfile detected: ${path.basename(lockFile)}`,
detail: 'You must use pnpm! Do you want to convert now?',
buttons: ['Convert to pnpm', 'Delete Lockfile', 'Ignore'],
defaultId: 0
}).then(result => {
if (result.response === 0) {
// Run conversion
} else if (result.response === 1) {
fs.unlinkSync(lockFile);
}
});
}

191
electron/features/scanner.ts Executable file
View file

@ -0,0 +1,191 @@
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
interface ScanResult {
path: string;
size: number;
lastAccessed: Date;
type: 'node_modules' | 'vendor' | 'venv';
}
export async function scanDirectory(rootDir: string, maxDepth: number = 5): Promise<ScanResult[]> {
const results: ScanResult[] = [];
async function traverse(currentPath: string, depth: number) {
if (depth > maxDepth) return;
try {
const entries = await fs.readdir(currentPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentPath, entry.name);
if (entry.isDirectory()) {
if (entry.name === 'node_modules' || entry.name === 'vendor' || entry.name === '.venv') {
// Found a target
try {
const stats = await fs.stat(fullPath);
results.push({
path: fullPath,
size: 0, // Calculating size is expensive, might do lazily or separate task
lastAccessed: stats.atime,
type: entry.name as any
});
// Don't traverse inside node_modules
continue;
} catch (e) {
console.error(`Error stat-ing ${fullPath}`, e);
}
} else if (!entry.name.startsWith('.')) {
// Recurse normal directories
await traverse(fullPath, depth + 1);
}
}
}
} catch (error) {
console.error(`Error scanning ${currentPath}`, error);
}
}
await traverse(rootDir, 0);
return results;
}
export async function getFolderSize(folderPath: string): Promise<number> {
let total = 0;
try {
const stats = await fs.stat(folderPath);
if (stats.isFile()) return stats.size;
const files = await fs.readdir(folderPath, { withFileTypes: true });
for (const file of files) {
total += await getFolderSize(path.join(folderPath, file.name));
}
} catch (e) {
// ignore errors
}
return total;
}
export interface DeepScanResult {
path: string;
size: number;
isDirectory: boolean;
}
export async function findLargeFiles(rootDir: string, threshold: number = 100 * 1024 * 1024): Promise<DeepScanResult[]> {
const results: DeepScanResult[] = [];
async function traverse(currentPath: string) {
try {
const stats = await fs.stat(currentPath);
if (stats.size > threshold && !stats.isDirectory()) {
results.push({ path: currentPath, size: stats.size, isDirectory: false });
return;
}
if (stats.isDirectory()) {
// SKIP node_modules to prevent self-deletion of the running app!
if (path.basename(currentPath) === 'node_modules') return;
const entries = await fs.readdir(currentPath, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith('.') && entry.name !== '.Trash') continue;
await traverse(path.join(currentPath, entry.name));
}
}
} catch (e) { /* skip */ }
}
await traverse(rootDir);
return results.sort((a, b) => b.size - a.size);
}
export async function getDeepDiveSummary() {
const home = os.homedir();
const targets = [
path.join(home, 'Downloads'),
path.join(home, 'Documents'),
path.join(home, 'Desktop'),
path.join(home, 'Library/Application Support'),
];
const results: DeepScanResult[] = [];
for (const t of targets) {
console.log(`Scanning ${t}...`);
const large = await findLargeFiles(t, 50 * 1024 * 1024); // 50MB+
console.log(`Found ${large.length} large files in ${t}`);
results.push(...large);
}
return results.slice(0, 20); // Top 20
}
import { exec } from 'child_process';
import util from 'util';
const execPromise = util.promisify(exec);
export async function getDiskUsage() {
try {
// macOS/Linux: df -k / gives 1K-blocks
const { stdout } = await execPromise('df -k /');
const lines = stdout.trim().split('\n');
// Filesystem 1024-blocks Used Available Capacity iused ifree %iused Mounted on
// /dev/disk3s1s1 488245288 15266888 308805360 5% 350280 1957260560 0% /
if (lines.length < 2) return null;
const parts = lines[1].split(/\s+/);
// parts[1] is total in 1K blocks
// parts[2] is used
// parts[3] is available
const total = parseInt(parts[1]) * 1024;
const used = parseInt(parts[2]) * 1024;
const available = parseInt(parts[3]) * 1024;
return {
totalGB: (total / 1024 / 1024 / 1024).toFixed(2),
usedGB: (used / 1024 / 1024 / 1024).toFixed(2),
freeGB: (available / 1024 / 1024 / 1024).toFixed(2)
};
} catch (e) {
console.error("Error getting disk usage:", e);
return null;
}
}
export async function findHeavyFolders(rootDir: string): Promise<DeepScanResult[]> {
try {
console.log(`Deepest scan on: ${rootDir}`);
// du -k -d 2: report size in KB, max depth 2
// sort -nr: numeric reverse sort
// head -n 50: top 50
const { stdout } = await execPromise(`du -k -d 2 "${rootDir}" | sort -nr | head -n 50`);
const lines = stdout.trim().split('\n');
const results = lines.map(line => {
// Trim leading whitespace
const trimmed = line.trim();
// Split by first whitespace only
const firstSpace = trimmed.indexOf('\t'); // du output is usually tab separated or space
// Actually du output on mac is "size<tab>path"
// Robust splitting for size and path
const match = trimmed.match(/^(\d+)\s+(.+)$/);
if (!match) return null;
const sizeK = parseInt(match[1]);
const fullPath = match[2];
return {
path: fullPath,
size: sizeK * 1024, // Convert KB to Bytes
isDirectory: true
};
}).filter(item => item !== null && item.path !== rootDir) as DeepScanResult[];
return results;
} catch (e) {
console.error("Deepest scan failed:", e);
return [];
}
}

43
electron/features/updater.ts Executable file
View file

@ -0,0 +1,43 @@
import { exec } from 'child_process';
import util from 'util';
const execAsync = util.promisify(exec);
// Stasis Field: macOS Update Control
// Requires sudo for most operations.
export async function disableAutoUpdates(password?: string) {
// Command to disable auto-update
// defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticCheckEnabled -bool false
// softwareupdate --schedule off
const cmds = [
'sudo -S softwareupdate --schedule off',
'sudo -S defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticCheckEnabled -bool false',
'sudo -S defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticDownload -bool false',
'sudo -S defaults write /Library/Preferences/com.apple.commerce AutoUpdate -bool false'
];
// Note: Handling password securely is tricky via IPC.
// Usually we prompt via a sudo-capable executor (like osascript with administrator privileges).
try {
await execWithSudo('softwareupdate --schedule off');
// More commands...
return true;
} catch (error) {
console.error('Failed to disable updates', error);
return false;
}
}
async function execWithSudo(command: string) {
const script = `do shell script "${command}" with administrator privileges`;
return execAsync(`osascript -e '${script}'`);
}
export async function ignoreUpdate(label: string) {
// softwareupdate --ignore <label>
// Note: --ignore flag is deprecated/removed in recent macOS for major updates, but might work for minor.
return execWithSudo(`softwareupdate --ignore "${label}"`);
}

244
electron/main.ts Executable file
View file

@ -0,0 +1,244 @@
import { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage } from 'electron';
import fs from 'fs';
import path from 'path';
import { spawn, ChildProcess } from 'child_process';
import { scanDirectory, getDeepDiveSummary, getDiskUsage, findHeavyFolders } from './features/scanner.js';
import { startEnforcement } from './features/enforcer.js';
import { disableAutoUpdates, ignoreUpdate } from './features/updater.js';
import { clearCaches, emptyTrash, cleanupDocker, cleanupTmp, purgePath, cleanupXcode, cleanupTurnkey } from './features/cleaner.js';
// In CJS, __dirname is globally defined. We rely on esbuild providing it.
// To stop TypeScript from complaining about missing type in 'module' mode:
declare const __dirname: string;
declare const __filename: string;
let mainWindow: BrowserWindow | null = null;
let backendProcess: ChildProcess | null = null;
let tray: Tray | null = null;
const startBackend = () => {
if (process.env.NODE_ENV === 'development') {
console.log('Development mode: Backend should be running via start-go.sh');
return;
}
const backendPath = path.join(process.resourcesPath, 'backend');
console.log('Starting backend from:', backendPath);
try {
backendProcess = spawn(backendPath, [], {
stdio: 'inherit'
});
backendProcess.on('error', (err) => {
console.error('Failed to start backend:', err);
});
backendProcess.on('exit', (code, signal) => {
console.log(`Backend exited with code ${code} and signal ${signal}`);
});
} catch (error) {
console.error('Error spawning backend:', error);
}
};
function createTray() {
const iconPath = path.join(__dirname, '../dist/tray/tray-iconTemplate.png');
// Use Template image for automatic Light/Dark mode adjustment.
// Fallback to color if needed, but Template is best for menu bar.
// Check if dist/tray exists, if not try public/tray (dev mode)
let finalIconPath = iconPath;
if (!fs.existsSync(iconPath)) {
finalIconPath = path.join(__dirname, '../public/tray/tray-iconTemplate.png');
}
const image = nativeImage.createFromPath(finalIconPath);
tray = new Tray(image.resize({ width: 16, height: 16 }));
tray.setToolTip('Antigravity Cleaner');
updateTrayMenu('Initializing...');
}
let isDockVisible = true;
function updateTrayMenu(statusText: string) {
if (!tray) return;
const contextMenu = Menu.buildFromTemplate([
{ label: `Storage: ${statusText}`, enabled: false },
{ type: 'separator' },
{
label: 'Open Dashboard', click: () => {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
}
},
{
label: 'Free Up Storage', click: () => {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
// Optionally send IPC to trigger scan?
}
}
},
{ type: 'separator' },
{
label: 'Show Dock Icon',
type: 'checkbox',
checked: isDockVisible,
click: (menuItem) => {
isDockVisible = menuItem.checked;
if (isDockVisible) {
app.dock.show();
} else {
app.dock.hide();
}
}
},
{ type: 'separator' },
{ label: 'Quit', click: () => app.quit() }
]);
tray.setContextMenu(contextMenu);
// Do NOT reset Title if we are just updating the menu structure,
// but statusText drives the menu so we keep it.
tray.setTitle(statusText);
}
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
backgroundColor: '#FFFFFF', // Helps prevent white flash
webPreferences: {
preload: path.join(__dirname, 'preload.cjs'),
nodeIntegration: true,
contextIsolation: true,
},
});
const isDev = process.env.NODE_ENV === 'development';
const port = process.env.PORT || 5173;
if (isDev) {
mainWindow.loadURL(`http://localhost:${port}`);
// mainWindow.webContents.openDevTools();
} else {
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'));
}
mainWindow.on('closed', () => {
mainWindow = null;
});
}
app.whenReady().then(() => {
// IPC Handlers
ipcMain.handle('scan-directory', async (event, path) => {
return scanDirectory(path);
});
ipcMain.handle('deep-dive-scan', async () => {
return getDeepDiveSummary();
});
ipcMain.handle('get-disk-usage', async () => {
return getDiskUsage();
});
ipcMain.handle('deepest-scan', async (event, targetPath) => {
// Default to Documents if no path provided, or allow passing path
const target = targetPath || path.join(app.getPath('home'), 'Documents');
return findHeavyFolders(target);
});
ipcMain.handle('disable-updates', async () => {
return disableAutoUpdates();
});
ipcMain.handle('clean-system', async () => {
return clearCaches(); // and others
});
ipcMain.handle('cleanup-docker', async () => {
return cleanupDocker();
});
ipcMain.handle('cleanup-tmp', async () => {
return cleanupTmp();
});
ipcMain.handle('cleanup-xcode', async () => {
return cleanupXcode();
});
ipcMain.handle('cleanup-turnkey', async () => {
return cleanupTurnkey();
});
ipcMain.handle('purge-path', async (event, targetPath) => {
// Security check: ensure path is within user home or projects?
// For now, allow it but in production we need safeguards.
// Also import purgePath from cleaner.ts (need to update imports)
// const { purgePath } = await import('./features/cleaner.js'); // Already imported above
return purgePath(targetPath);
});
// Start background enforcer (example: monitoring home projects)
// startEnforcement('/Users/khoa.vo/Projects'); // TODO: Make configurable
// IPC for Tray
ipcMain.handle('update-tray-title', (event, title) => {
if (tray) {
tray.setTitle(title);
updateTrayMenu(title); // Update menu with new status too if needed
}
});
ipcMain.handle('get-app-icon', async (event, appPath) => {
try {
const icon = await app.getFileIcon(appPath, { size: 'normal' });
return icon.toDataURL();
} catch (e) {
console.error('Failed to get icon for:', appPath, e);
return '';
return '';
}
});
ipcMain.handle('update-tray-icon', (event, dataUrl) => {
if (tray && dataUrl) {
const image = nativeImage.createFromDataURL(dataUrl);
// Resize to tray standard (16x16 or 22x22 usually, let's keep it crisp)
tray.setImage(image.resize({ width: 22, height: 22 }));
}
});
createWindow();
createTray(); // Initialize Tray
startBackend();
});
app.on('will-quit', () => {
if (backendProcess) {
console.log('Killing backend process...');
backendProcess.kill();
backendProcess = null;
}
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (mainWindow === null) {
createWindow();
}
});

18
electron/preload.ts Executable file
View file

@ -0,0 +1,18 @@
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('electronAPI', {
scanDirectory: (path: string) => ipcRenderer.invoke('scan-directory', path),
disableUpdates: () => ipcRenderer.invoke('disable-updates'),
cleanSystem: () => ipcRenderer.invoke('clean-system'),
purgePath: (path: string) => ipcRenderer.invoke('purge-path', path),
cleanupDocker: () => ipcRenderer.invoke('cleanup-docker'),
cleanupTmp: () => ipcRenderer.invoke('cleanup-tmp'),
cleanupXcode: () => ipcRenderer.invoke('cleanup-xcode'),
cleanupTurnkey: () => ipcRenderer.invoke('cleanup-turnkey'),
deepDiveScan: () => ipcRenderer.invoke('deep-dive-scan'),
getDiskUsage: () => ipcRenderer.invoke('get-disk-usage'),
deepestScan: (path?: string) => ipcRenderer.invoke('deepest-scan', path),
updateTrayTitle: (title: string) => ipcRenderer.invoke('update-tray-title', title),
getAppIcon: (path: string) => ipcRenderer.invoke('get-app-icon', path),
updateTrayIcon: (dataUrl: string) => ipcRenderer.invoke('update-tray-icon', dataUrl),
});

14
electron/tsconfig.json Executable file
View file

@ -0,0 +1,14 @@
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ESNext",
"outDir": "../dist-electron",
"rootDir": ".",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["."]
}

23
eslint.config.js Executable file
View file

@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module github.com/kv/clearnup
go 1.25.4

13
index.html Executable file
View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>kv-clearnup</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

7447
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

81
package.json Executable file
View file

@ -0,0 +1,81 @@
{
"name": "Lumina",
"private": true,
"version": "0.0.0",
"type": "module",
"main": "dist-electron/main.cjs",
"scripts": {
"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\"",
"electron:build": "node scripts/build-electron.mjs",
"build": "tsc -b && vite build",
"build:go:mac": "sh scripts/build-go.sh",
"build:mac": "npm run build:go:mac && npm run build && npm run electron:build && electron-builder --mac --universal",
"lint": "eslint .",
"preview": "vite preview",
"preinstall": "node scripts/check-pnpm.js"
},
"dependencies": {
"clsx": "^2.1.1",
"framer-motion": "^12.29.2",
"lucide-react": "^0.563.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwind-merge": "^3.4.0"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"autoprefixer": "^10.4.20",
"concurrently": "^9.1.0",
"cross-env": "^7.0.3",
"electron": "^33.2.1",
"electron-builder": "^26.4.0",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.17",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.4",
"vite": "^7.2.4",
"wait-on": "^8.0.1"
},
"pnpm": {
"onlyBuiltDependencies": [
"electron",
"esbuild"
]
},
"build": {
"appId": "com.kv.clearnup",
"productName": "KV Clearnup",
"directories": {
"output": "release"
},
"compression": "maximum",
"mac": {
"target": [
"dmg"
],
"icon": "build/icon.png",
"category": "public.app-category.utilities",
"hardenedRuntime": false
},
"files": [
"dist/**/*",
"dist-electron/**/*",
"package.json"
],
"extraResources": [
{
"from": "backend/dist/universal/backend",
"to": "backend"
}
]
}
}

3451
pnpm-lock.yaml Executable file

File diff suppressed because it is too large Load diff

7
postcss.config.js Executable file
View file

@ -0,0 +1,7 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
// Force reload

BIN
public/tray/tray-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

1
public/vite.svg Executable file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

40
release/builder-debug.yml Normal file
View file

@ -0,0 +1,40 @@
x64:
firstOrDefaultFilePatterns:
- '!**/node_modules/**'
- '!build{,/**/*}'
- '!release{,/**/*}'
- dist/**/*
- dist-electron/**/*
- package.json
- '!**/*.{iml,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,suo,xproj,cc,d.ts,mk,a,o,obj,forge-meta,pdb}'
- '!**/._*'
- '!**/electron-builder.{yaml,yml,json,json5,toml,ts}'
- '!**/{.git,.hg,.svn,CVS,RCS,SCCS,__pycache__,.DS_Store,thumbs.db,.gitignore,.gitkeep,.gitattributes,.npmignore,.idea,.vs,.flowconfig,.jshintrc,.eslintrc,.circleci,.yarn-integrity,.yarn-metadata.json,yarn-error.log,yarn.lock,package-lock.json,npm-debug.log,pnpm-lock.yaml,bun.lock,bun.lockb,appveyor.yml,.travis.yml,circle.yml,.nyc_output,.husky,.github,electron-builder.env}'
- '!.yarn{,/**/*}'
- '!.editorconfig'
- '!.yarnrc.yml'
nodeModuleFilePatterns:
- '**/*'
- dist/**/*
- dist-electron/**/*
- package.json
arm64:
firstOrDefaultFilePatterns:
- '!**/node_modules/**'
- '!build{,/**/*}'
- '!release{,/**/*}'
- dist/**/*
- dist-electron/**/*
- package.json
- '!**/*.{iml,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,suo,xproj,cc,d.ts,mk,a,o,obj,forge-meta,pdb}'
- '!**/._*'
- '!**/electron-builder.{yaml,yml,json,json5,toml,ts}'
- '!**/{.git,.hg,.svn,CVS,RCS,SCCS,__pycache__,.DS_Store,thumbs.db,.gitignore,.gitkeep,.gitattributes,.npmignore,.idea,.vs,.flowconfig,.jshintrc,.eslintrc,.circleci,.yarn-integrity,.yarn-metadata.json,yarn-error.log,yarn.lock,package-lock.json,npm-debug.log,pnpm-lock.yaml,bun.lock,bun.lockb,appveyor.yml,.travis.yml,circle.yml,.nyc_output,.husky,.github,electron-builder.env}'
- '!.yarn{,/**/*}'
- '!.editorconfig'
- '!.yarnrc.yml'
nodeModuleFilePatterns:
- '**/*'
- dist/**/*
- dist-electron/**/*
- package.json

View file

@ -0,0 +1,21 @@
directories:
output: release
buildResources: build
appId: com.kv.clearnup
productName: KV Clearnup
compression: maximum
mac:
target:
- dmg
icon: build/icon.png
category: public.app-category.utilities
hardenedRuntime: false
files:
- filter:
- dist/**/*
- dist-electron/**/*
- package.json
extraResources:
- from: backend/dist/universal/backend
to: backend
electronVersion: 33.4.11

View file

@ -0,0 +1 @@
Versions/Current/Electron Framework

View file

@ -0,0 +1 @@
Versions/Current/Helpers

View file

@ -0,0 +1 @@
Versions/Current/Libraries

View file

@ -0,0 +1 @@
Versions/Current/Resources

View file

@ -0,0 +1 @@
{"file_format_version": "1.0.0", "ICD": {"library_path": "./libvk_swiftshader.dylib", "api_version": "1.0.5"}}

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>Electron Framework</string>
<key>CFBundleIdentifier</key>
<string>com.github.Electron.framework</string>
<key>CFBundleName</key>
<string>Electron Framework</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleVersion</key>
<string>33.4.11</string>
<key>DTCompiler</key>
<string>com.apple.compilers.llvm.clang.1_0</string>
<key>DTSDKBuild</key>
<string>23F73</string>
<key>DTSDKName</key>
<string>macosx14.5</string>
<key>DTXcode</key>
<string>1540</string>
<key>DTXcodeBuild</key>
<string>15F31d</string>
<key>LSEnvironment</key>
<dict>
<key>MallocNanoZone</key>
<string>0</string>
</dict>
<key>NSSupportsAutomaticGraphicsSwitching</key>
<true/>
<key>ElectronAsarIntegrity</key>
<dict>
<key>Resources/app.asar</key>
<dict>
<key>algorithm</key>
<string>SHA256</string>
<key>hash</key>
<string>7f0ca3c6fae4ccfe2d088e243546c0f695b844fbf714bd59e8c6111fb873f334</string>
</dict>
</dict>
</dict>
</plist>

Some files were not shown because too many files have changed in this diff Show more