feat: add portable release script and cleanup project

This commit is contained in:
vndangkhoa 2026-02-02 08:15:51 +07:00
parent 301e638900
commit 917c1b043c
179 changed files with 3490 additions and 3716 deletions

7
.gitignore vendored Executable file → Normal file
View file

@ -22,3 +22,10 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
*.sw?
# Release Artifacts
Release/
*.zip
*.exe
backend/dist/

0
.npmrc Executable file → Normal file
View file

0
README.md Executable file → Normal file
View file

View file

@ -0,0 +1,22 @@
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,3 +1,5 @@
//go:build darwin
package apps package apps
import ( import (
@ -9,25 +11,7 @@ import (
"sync" "sync"
) )
type AppInfo struct { // Structs moved to apps_common.go
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) {
@ -81,7 +65,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")
@ -215,3 +199,8 @@ 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

@ -0,0 +1,248 @@
//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

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

View file

@ -0,0 +1,114 @@
//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

@ -0,0 +1,106 @@
//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

@ -0,0 +1,92 @@
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,3 +1,5 @@
//go:build darwin
package scanner package scanner
import ( import (
@ -10,33 +12,10 @@ import (
"strings" "strings"
) )
type ScanResult struct { // Structs moved to scanner_common.go
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 {
@ -80,59 +59,15 @@ 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 walks a directory and returns files > threshold // FindLargeFiles moved to scanner_common.go
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) {
@ -384,10 +319,7 @@ func GetCategorySizes() (*CategorySizes, error) {
return sizes, nil return sizes, nil
} }
type CleaningEstimates struct { // CleaningEstimates struct moved to scanner_common.go
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

@ -0,0 +1,435 @@
//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

@ -0,0 +1,38 @@
//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"),
}
default:
return []string{}
}
}

View file

@ -0,0 +1,90 @@
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,8 +1,11 @@
package main package main
import ( import (
"embed"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"io/fs"
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
@ -11,9 +14,13 @@ import (
"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/platform"
"github.com/kv/clearnup/backend/internal/scanner" "github.com/kv/clearnup/backend/internal/scanner"
) )
//go:embed all:dist
var distFS embed.FS
const Port = ":36969" const Port = ":36969"
func enableCors(w *http.ResponseWriter) { func enableCors(w *http.ResponseWriter) {
@ -41,8 +48,52 @@ func main() {
http.HandleFunc("/api/apps", handleScanApps) http.HandleFunc("/api/apps", handleScanApps)
http.HandleFunc("/api/apps/details", handleAppDetails) http.HandleFunc("/api/apps/details", handleAppDetails)
http.HandleFunc("/api/apps/action", handleAppAction) http.HandleFunc("/api/apps/action", handleAppAction)
http.HandleFunc("/api/apps/uninstall", handleAppUninstall)
// Static File Serving (SPA Support)
// Check if we are running with embedded files or local dist
// Priority: Embedded (Production) -> Local dist (Dev/Preview)
// Try to get a sub-fs for "dist" from the embedded FS
distRoot, err := fs.Sub(distFS, "dist")
if err == nil {
fmt.Println("📂 Serving embedded static files")
// Check if it's actually populated (sanity check)
if _, err := distRoot.Open("index.html"); err == nil {
fsrv := http.FileServer(http.FS(distRoot))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if filepath.Ext(r.URL.Path) == "" {
// Read index.html from embedded
index, _ := distRoot.Open("index.html")
stat, _ := index.Stat()
http.ServeContent(w, r, "index.html", stat.ModTime(), index.(io.ReadSeeker))
return
}
fsrv.ServeHTTP(w, r)
})
} else {
// Fallback to local ./dist if embedded is empty (e.g. dev mode without build)
if _, err := os.Stat("dist"); err == nil {
fmt.Println("📂 Serving static files from local ./dist")
fs := http.FileServer(http.Dir("dist"))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if filepath.Ext(r.URL.Path) == "" {
http.ServeFile(w, r, "dist/index.html")
return
}
fs.ServeHTTP(w, r)
})
}
}
}
fmt.Printf("🚀 Antigravity Backend running on http://localhost%s\n", Port) fmt.Printf("🚀 Antigravity Backend running on http://localhost%s\n", Port)
// Open Browser if not in development mode
if os.Getenv("APP_ENV") != "development" {
go platform.OpenBrowser("http://localhost" + Port)
}
if err := http.ListenAndServe(Port, nil); err != nil { if err := http.ListenAndServe(Port, nil); err != nil {
fmt.Printf("Server failed: %s\n", err) fmt.Printf("Server failed: %s\n", err)
} }
@ -64,38 +115,17 @@ func handleScanCategory(w http.ResponseWriter, r *http.Request) {
return return
} }
home, _ := os.UserHomeDir() targets := scanner.GetScanTargets(req.Category)
var targets []string if len(targets) == 0 {
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{}) json.NewEncoder(w).Encode([]scanner.ScanResult{})
return 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 var allResults []scanner.ScanResult
for _, t := range targets { for _, t := range targets {
if t == "" {
continue
}
res, _ := scanner.FindLargeFiles(t, 10*1024*1024) // 10MB res, _ := scanner.FindLargeFiles(t, 10*1024*1024) // 10MB
allResults = append(allResults, res...) allResults = append(allResults, res...)
} }
@ -117,9 +147,9 @@ func handleOpenSettings(w http.ResponseWriter, r *http.Request) {
return return
} }
// Open Storage Settings if err := platform.OpenSettings(); err != nil {
// macOS Ventura+: open x-apple.systempreferences:com.apple.settings.Storage fmt.Printf("Failed to open settings: %v\n", err)
exec.Command("open", "x-apple.systempreferences:com.apple.settings.Storage").Run() }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
} }
@ -134,6 +164,7 @@ func handleDiskUsage(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(usage) json.NewEncoder(w).Encode(usage)
} }
@ -230,26 +261,11 @@ func handleEmptyTrash(w http.ResponseWriter, r *http.Request) {
return return
} }
home, err := os.UserHomeDir() if err := platform.EmptyTrash(); err != nil {
if err != nil { http.Error(w, fmt.Sprintf("Cannot empty trash: %v", err), http.StatusInternalServerError)
http.Error(w, "Cannot get home directory", http.StatusInternalServerError)
return 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}) json.NewEncoder(w).Encode(map[string]bool{"success": true})
} }
@ -259,13 +275,16 @@ func handleClearCache(w http.ResponseWriter, r *http.Request) {
return return
} }
home, _ := os.UserHomeDir() cachePath, err := platform.GetCachePath()
cachePath := filepath.Join(home, "Library", "Caches") if err != nil {
http.Error(w, "Cannot get cache path", http.StatusInternalServerError)
return
}
// Get size before clearing // Get size before clearing
sizeBefore := scanner.GetDirectorySize(cachePath) sizeBefore := scanner.GetDirectorySize(cachePath)
// Clear cache directories (keep the Caches folder itself) // Clear cache directories (keep the Caches folder itself if possible, or jus remove content)
entries, err := os.ReadDir(cachePath) entries, err := os.ReadDir(cachePath)
if err != nil { if err != nil {
http.Error(w, "Cannot read cache directory", http.StatusInternalServerError) http.Error(w, "Cannot read cache directory", http.StatusInternalServerError)
@ -286,33 +305,17 @@ func handleCleanDocker(w http.ResponseWriter, r *http.Request) {
return return
} }
// Try to find docker executable dockerPath, err := platform.GetDockerPath()
dockerPath, err := exec.LookPath("docker")
if err != nil { 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{}{ json.NewEncoder(w).Encode(map[string]interface{}{
"cleared": 0, "cleared": 0,
"message": "Docker not found in PATH or common locations", "message": "Docker not found",
}) })
return return
} }
// Run docker system prune -af // Run docker system prune -af --volumes to clean images, containers, and volumes
cmd := exec.Command(dockerPath, "system", "prune", "-af") cmd := exec.Command(dockerPath, "system", "prune", "-af", "--volumes")
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
@ -335,52 +338,13 @@ func handleSystemInfo(w http.ResponseWriter, r *http.Request) {
return return
} }
// Structs for parsing system_profiler JSON info, err := platform.GetSystemInfo()
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 { if err != nil {
http.Error(w, "Failed to get system info", http.StatusInternalServerError) http.Error(w, "Failed to get system info", http.StatusInternalServerError)
return return
} }
var profile SystemProfile json.NewEncoder(w).Encode(info)
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) { func handleCleaningEstimates(w http.ResponseWriter, r *http.Request) {
@ -413,23 +377,23 @@ func handleScanApps(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(appsList) json.NewEncoder(w).Encode(appsList)
} }
type AppDetailsRequest struct {
Path string `json:"path"`
}
func handleAppDetails(w http.ResponseWriter, r *http.Request) { func handleAppDetails(w http.ResponseWriter, r *http.Request) {
enableCors(&w) enableCors(&w)
if r.Method == "OPTIONS" { if r.Method == "OPTIONS" {
return return
} }
type AppDetailsRequest struct {
Path string `json:"path"`
BundleID string `json:"bundleID"`
}
var req AppDetailsRequest var req AppDetailsRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest) http.Error(w, "Invalid request body", http.StatusBadRequest)
return return
} }
details, err := apps.GetAppDetails(req.Path) details, err := apps.GetAppDetails(req.Path, req.BundleID)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -437,17 +401,15 @@ func handleAppDetails(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(details) json.NewEncoder(w).Encode(details)
} }
type AppActionRequest struct {
Files []string `json:"files"`
}
func handleAppAction(w http.ResponseWriter, r *http.Request) { func handleAppAction(w http.ResponseWriter, r *http.Request) {
enableCors(&w) enableCors(&w)
if r.Method == "OPTIONS" { if r.Method == "OPTIONS" {
return return
} }
var req AppActionRequest var req struct {
Files []string `json:"files"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest) http.Error(w, "Invalid request body", http.StatusBadRequest)
return return
@ -461,3 +423,26 @@ func handleAppAction(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]bool{"success": true}) json.NewEncoder(w).Encode(map[string]bool{"success": true})
} }
func handleAppUninstall(w http.ResponseWriter, r *http.Request) {
enableCors(&w)
if r.Method == "OPTIONS" {
return
}
var req struct {
Cmd string `json:"cmd"`
}
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})
}

BIN
backend/verify_output.txt Normal file

Binary file not shown.

View file

@ -0,0 +1,25 @@
$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
}

62
build-release.ps1 Normal file
View file

@ -0,0 +1,62 @@
# 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

@ -1,467 +0,0 @@
"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();
}
});

View file

@ -1,20 +0,0 @@
"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)
});

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

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

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

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

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

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

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

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

2
go.mod
View file

@ -1,3 +1,5 @@
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 Normal file
View file

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

0
index.html Executable file → Normal file
View file

0
package.json Executable file → Normal file
View file

1683
pnpm-lock.yaml Executable file → Normal file

File diff suppressed because it is too large Load diff

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

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

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

View file

@ -1,40 +0,0 @@
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

@ -1,21 +0,0 @@
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

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

View file

@ -1,43 +0,0 @@
<?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