feat: add portable release script and cleanup project
This commit is contained in:
parent
301e638900
commit
917c1b043c
179 changed files with 3490 additions and 3716 deletions
7
.gitignore
vendored
Executable file → Normal file
7
.gitignore
vendored
Executable file → Normal 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
0
.npmrc
Executable file → Normal file
0
README.md
Executable file → Normal file
0
README.md
Executable file → Normal file
22
backend/internal/apps/apps_common.go
Normal file
22
backend/internal/apps/apps_common.go
Normal 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"`
|
||||||
|
}
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
248
backend/internal/apps/apps_windows.go
Normal file
248
backend/internal/apps/apps_windows.go
Normal 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
|
||||||
|
}
|
||||||
8
backend/internal/platform/platform_common.go
Normal file
8
backend/internal/platform/platform_common.go
Normal 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"`
|
||||||
|
}
|
||||||
114
backend/internal/platform/platform_darwin.go
Normal file
114
backend/internal/platform/platform_darwin.go
Normal 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()
|
||||||
|
}
|
||||||
106
backend/internal/platform/platform_windows.go
Normal file
106
backend/internal/platform/platform_windows.go
Normal 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()
|
||||||
|
}
|
||||||
92
backend/internal/scanner/scanner_common.go
Normal file
92
backend/internal/scanner/scanner_common.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
435
backend/internal/scanner/scanner_windows.go
Normal file
435
backend/internal/scanner/scanner_windows.go
Normal 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
|
||||||
|
}
|
||||||
38
backend/internal/scanner/targets_darwin.go
Normal file
38
backend/internal/scanner/targets_darwin.go
Normal 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{}
|
||||||
|
}
|
||||||
|
}
|
||||||
90
backend/internal/scanner/targets_windows.go
Normal file
90
backend/internal/scanner/targets_windows.go
Normal 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{}
|
||||||
|
}
|
||||||
|
}
|
||||||
225
backend/main.go
225
backend/main.go
|
|
@ -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
BIN
backend/verify_output.txt
Normal file
Binary file not shown.
25
backend/verify_windows.ps1
Normal file
25
backend/verify_windows.ps1
Normal 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
62
build-release.ps1
Normal 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
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -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
0
electron/features/cleaner.ts
Executable file → Normal file
0
electron/features/enforcer.ts
Executable file → Normal file
0
electron/features/enforcer.ts
Executable file → Normal file
0
electron/features/scanner.ts
Executable file → Normal file
0
electron/features/scanner.ts
Executable file → Normal file
0
electron/features/updater.ts
Executable file → Normal file
0
electron/features/updater.ts
Executable file → Normal file
0
electron/main.ts
Executable file → Normal file
0
electron/main.ts
Executable file → Normal file
0
electron/preload.ts
Executable file → Normal file
0
electron/preload.ts
Executable file → Normal file
0
electron/tsconfig.json
Executable file → Normal file
0
electron/tsconfig.json
Executable file → Normal file
0
eslint.config.js
Executable file → Normal file
0
eslint.config.js
Executable file → Normal file
2
go.mod
2
go.mod
|
|
@ -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
2
go.sum
Normal 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
0
index.html
Executable file → Normal file
0
package.json
Executable file → Normal file
0
package.json
Executable file → Normal file
1683
pnpm-lock.yaml
Executable file → Normal 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
0
postcss.config.js
Executable file → Normal file
0
public/vite.svg
Executable file → Normal file
0
public/vite.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Versions/Current/Electron Framework
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Versions/Current/Helpers
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Versions/Current/Libraries
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Versions/Current/Resources
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1 +0,0 @@
|
||||||
{"file_format_version": "1.0.0", "ICD": {"library_path": "./libvk_swiftshader.dylib", "api_version": "1.0.5"}}
|
|
||||||
|
|
@ -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>
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue