Compare commits
No commits in common. "main" and "v1" have entirely different histories.
184 changed files with 9028 additions and 9394 deletions
54
.gitignore
vendored
Normal file → Executable file
54
.gitignore
vendored
Normal file → Executable file
|
|
@ -1,30 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
# Release Artifacts
|
||||
Release/
|
||||
release/
|
||||
*.zip
|
||||
*.exe
|
||||
backend/dist/
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
|
|||
0
.npmrc
Normal file → Executable file
0
.npmrc
Normal file → Executable file
135
README.md
Normal file → Executable file
135
README.md
Normal file → Executable file
|
|
@ -1,67 +1,68 @@
|
|||
# KV Clearnup (Antigravity) 🚀
|
||||
|
||||
A modern, high-performance system optimizer for macOS, built with **Electron**, **React**, and **Go**.
|
||||
|
||||

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

|
||||
|
||||
## Features
|
||||
- **Flash Clean**: Instantly remove system caches, logs, and trash.
|
||||
- **Deep Clean**: Scan for large files and heavy folders.
|
||||
- **Real-time Monitoring**: Track disk usage and category sizes.
|
||||
- **Universal Binary**: Runs natively on both Apple Silicon (M1/M2/M3) and Intel Macs.
|
||||
- **High Performance**: Heavy lifting is handled by a compiled Go backend.
|
||||
|
||||
## Prerequisites
|
||||
- **Node.js** (v18+)
|
||||
- **Go** (v1.20+)
|
||||
- **pnpm** (preferred) or npm
|
||||
|
||||
## Development
|
||||
|
||||
### 1. Install Dependencies
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. Run in Development Mode
|
||||
This starts the Go backend (port 36969) and the Vite/Electron frontend concurrently.
|
||||
```bash
|
||||
./start-go.sh
|
||||
```
|
||||
*Note: Do not run `npm run dev` directly if you want the backend to work. Use the script.*
|
||||
|
||||
## Building for Production
|
||||
|
||||
To create a distributable `.dmg` file for macOS:
|
||||
|
||||
### 1. Build the App
|
||||
```bash
|
||||
npm run build:mac
|
||||
```
|
||||
This command will:
|
||||
1. Compile the Go backend for both `amd64` and `arm64`.
|
||||
2. Create a universal binary using `lipo`.
|
||||
3. Build the React frontend.
|
||||
4. Package the Electron app and bundle the backend.
|
||||
5. Generate a universal `.dmg`.
|
||||
|
||||
### 2. Locate the Installer
|
||||
The output file will be at:
|
||||
```
|
||||
release/KV Clearnup-0.0.0-universal.dmg
|
||||
```
|
||||
|
||||
## Running the App
|
||||
1. **Mount the DMG**: Double-click the `.dmg` file in the `release` folder.
|
||||
2. **Install**: Drag the app to your `Applications` folder.
|
||||
3. **Launch**: Open "KV Clearnup" from Applications.
|
||||
|
||||
*Troubleshooting*: If you see "System Extension Blocked" or similar OS warnings, go to **System Settings > Privacy & Security** and allow the application.
|
||||
|
||||
## Architecture
|
||||
- **Frontend**: React, TypeScript, TailwindCSS, Framer Motion.
|
||||
- **Main Process**: Electron (TypeScript).
|
||||
- **Backend**: Go (Golang) for file system operations and heavy scanning.
|
||||
- **Communication**: Electron uses `child_process` to spawn the Go binary. Frontend communicates with backend via HTTP (localhost:36969).
|
||||
|
||||
## License
|
||||
MIT
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
//go:build darwin
|
||||
|
||||
package apps
|
||||
|
||||
import (
|
||||
|
|
@ -11,7 +9,25 @@ import (
|
|||
"sync"
|
||||
)
|
||||
|
||||
// Structs moved to apps_common.go
|
||||
type AppInfo struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
BundleID string `json:"bundleID"`
|
||||
Size int64 `json:"size"`
|
||||
Icon string `json:"icon,omitempty"` // Base64 or path? For now just path to .app (frontend can get icon)
|
||||
}
|
||||
|
||||
type AssociatedFile struct {
|
||||
Path string `json:"path"`
|
||||
Type string `json:"type"` // "cache", "config", "log", "data"
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
type AppDetails struct {
|
||||
AppInfo
|
||||
Associated []AssociatedFile `json:"associated"`
|
||||
TotalSize int64 `json:"totalSize"`
|
||||
}
|
||||
|
||||
// ScanApps returns a list of installed applications
|
||||
func ScanApps() ([]AppInfo, error) {
|
||||
|
|
@ -65,7 +81,7 @@ func ScanApps() ([]AppInfo, error) {
|
|||
}
|
||||
|
||||
// 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)
|
||||
if bid == "" {
|
||||
return nil, fmt.Errorf("could not determine bundle ID")
|
||||
|
|
@ -199,8 +215,3 @@ func getType(locName string) string {
|
|||
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")
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
package apps
|
||||
|
||||
type AppInfo struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
BundleID string `json:"bundleID"` // On Windows this can be ProductCode or Registry Key Name
|
||||
UninstallString string `json:"uninstallString"`
|
||||
Size int64 `json:"size"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
}
|
||||
|
||||
type AssociatedFile struct {
|
||||
Path string `json:"path"`
|
||||
Type string `json:"type"` // "cache", "config", "log", "data"
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
type AppDetails struct {
|
||||
AppInfo
|
||||
Associated []AssociatedFile `json:"associated"`
|
||||
TotalSize int64 `json:"totalSize"`
|
||||
}
|
||||
|
|
@ -1,248 +0,0 @@
|
|||
//go:build windows
|
||||
|
||||
package apps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
// ScanApps returns a list of installed applications via Registry
|
||||
func ScanApps() ([]AppInfo, error) {
|
||||
var apps []AppInfo
|
||||
|
||||
// Keys to search
|
||||
// HKLM Software\Microsoft\Windows\CurrentVersion\Uninstall
|
||||
// HKLM Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall
|
||||
// HKCU Software\Microsoft\Windows\CurrentVersion\Uninstall
|
||||
|
||||
keys := []struct {
|
||||
hive registry.Key
|
||||
path string
|
||||
}{
|
||||
{registry.LOCAL_MACHINE, `Software\Microsoft\Windows\CurrentVersion\Uninstall`},
|
||||
{registry.LOCAL_MACHINE, `Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall`},
|
||||
{registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Uninstall`},
|
||||
}
|
||||
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for _, k := range keys {
|
||||
baseKey, err := registry.OpenKey(k.hive, k.path, registry.READ)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
subkeys, err := baseKey.ReadSubKeyNames(-1)
|
||||
baseKey.Close()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, subkeyName := range subkeys {
|
||||
appKey, err := registry.OpenKey(k.hive, k.path+`\`+subkeyName, registry.READ)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
displayName, _, err := appKey.GetStringValue("DisplayName")
|
||||
if err != nil || displayName == "" {
|
||||
appKey.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
// Define installLocation explicitly
|
||||
installLocation, _, _ := appKey.GetStringValue("InstallLocation")
|
||||
uninstallString, _, _ := appKey.GetStringValue("UninstallString")
|
||||
quietUninstallString, _, _ := appKey.GetStringValue("QuietUninstallString")
|
||||
|
||||
if uninstallString == "" && quietUninstallString != "" {
|
||||
uninstallString = quietUninstallString
|
||||
}
|
||||
|
||||
// Debug Log
|
||||
if strings.Contains(displayName, "Foxit") {
|
||||
fmt.Printf("found Foxit: %s | UninstallString: %s\n", displayName, uninstallString)
|
||||
}
|
||||
|
||||
// Deduplication: If we've seen this Name + Location combination, skip it.
|
||||
// This handles the common case of 32-bit apps appearing in both HKLM and WOW6432Node.
|
||||
dedupKey := displayName + "|" + strings.ToLower(installLocation)
|
||||
if seen[dedupKey] {
|
||||
appKey.Close()
|
||||
continue
|
||||
}
|
||||
seen[dedupKey] = true
|
||||
|
||||
// Try to get size from registry (EstimatedSize is in KB)
|
||||
sizeVal, _, errSize := appKey.GetIntegerValue("EstimatedSize")
|
||||
var sizeBytes int64
|
||||
if errSize == nil {
|
||||
sizeBytes = int64(sizeVal) * 1024
|
||||
}
|
||||
|
||||
// Construct Full Registry Key Path as BundleID for later use
|
||||
hiveName := "HKLM"
|
||||
if k.hive == registry.CURRENT_USER {
|
||||
hiveName = "HKCU"
|
||||
}
|
||||
fullRegPath := hiveName + `\` + k.path + `\` + subkeyName
|
||||
|
||||
apps = append(apps, AppInfo{
|
||||
Name: displayName,
|
||||
Path: installLocation,
|
||||
BundleID: fullRegPath,
|
||||
UninstallString: uninstallString,
|
||||
Size: sizeBytes,
|
||||
})
|
||||
appKey.Close()
|
||||
}
|
||||
}
|
||||
|
||||
return apps, nil
|
||||
}
|
||||
|
||||
// GetAppDetails finds all associated files (simplified for Windows)
|
||||
func GetAppDetails(appPath, bundleID string) (*AppDetails, error) {
|
||||
// appPath might come from ScanApps which set it to InstallLocation.
|
||||
// bundleID is used as the Registry Key Path.
|
||||
|
||||
// Re-construct basic info
|
||||
info := AppInfo{
|
||||
Name: filepath.Base(appPath),
|
||||
Path: appPath,
|
||||
BundleID: bundleID,
|
||||
// UninstallString is hard to recover if not passed, but usually we call GetAppDetails after ScanApps which has it.
|
||||
// For now leave empty, or we'd need to re-query registry if bundleID is a registry path.
|
||||
Size: 0,
|
||||
}
|
||||
|
||||
if appPath == "" && bundleID != "" {
|
||||
// Fallback name if path is empty
|
||||
parts := strings.Split(bundleID, `\`)
|
||||
if len(parts) > 0 {
|
||||
info.Name = parts[len(parts)-1]
|
||||
}
|
||||
}
|
||||
|
||||
details := &AppDetails{
|
||||
AppInfo: info,
|
||||
TotalSize: 0,
|
||||
}
|
||||
|
||||
// 1. Scan File System
|
||||
if appPath != "" {
|
||||
var size int64
|
||||
filepath.WalkDir(appPath, func(_ string, d os.DirEntry, err error) error {
|
||||
if err == nil && !d.IsDir() {
|
||||
i, _ := d.Info()
|
||||
size += i.Size()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
details.AppInfo.Size = size
|
||||
details.TotalSize = size
|
||||
|
||||
// Add the main folder as associated data
|
||||
details.Associated = append(details.Associated, AssociatedFile{
|
||||
Path: appPath,
|
||||
Type: "data",
|
||||
Size: size,
|
||||
})
|
||||
}
|
||||
|
||||
// 2. Add Registry Key (Uninstall Entry)
|
||||
if bundleID != "" && (strings.HasPrefix(bundleID, "HKLM") || strings.HasPrefix(bundleID, "HKCU")) {
|
||||
// We treat the registry key as a "file" with special type and 0 size
|
||||
details.Associated = append(details.Associated, AssociatedFile{
|
||||
Path: "REG:" + bundleID,
|
||||
Type: "registry", // New type
|
||||
Size: 0, // Registry entries are negligible in size
|
||||
})
|
||||
}
|
||||
|
||||
return details, nil
|
||||
}
|
||||
|
||||
// DeleteFiles removes the requested paths
|
||||
func DeleteFiles(paths []string) error {
|
||||
for _, p := range paths {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Registry Deletion
|
||||
if strings.HasPrefix(p, "REG:") {
|
||||
regPath := strings.TrimPrefix(p, "REG:")
|
||||
deleteRegistryKey(regPath)
|
||||
continue
|
||||
}
|
||||
|
||||
// Safety checks
|
||||
if p == "C:\\" || p == "c:\\" ||
|
||||
p == "C:\\Windows" || strings.HasPrefix(strings.ToLower(p), "c:\\windows") {
|
||||
continue
|
||||
}
|
||||
|
||||
err := os.RemoveAll(p)
|
||||
if err != nil {
|
||||
// Log error but continue? Or return?
|
||||
// return err
|
||||
// On Windows file locking is common, best effort
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteRegistryKey(fullPath string) error {
|
||||
var hive registry.Key
|
||||
var subPath string
|
||||
|
||||
if strings.HasPrefix(fullPath, "HKLM\\") {
|
||||
hive = registry.LOCAL_MACHINE
|
||||
subPath = strings.TrimPrefix(fullPath, "HKLM\\")
|
||||
} else if strings.HasPrefix(fullPath, "HKCU\\") {
|
||||
hive = registry.CURRENT_USER
|
||||
subPath = strings.TrimPrefix(fullPath, "HKCU\\")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Provide parent key and subkey name to DeleteKey
|
||||
// path: Software\...\Uninstall\AppGUID
|
||||
lastSlash := strings.LastIndex(subPath, `\`)
|
||||
if lastSlash == -1 {
|
||||
return nil
|
||||
}
|
||||
parentPath := subPath[:lastSlash]
|
||||
keyName := subPath[lastSlash+1:]
|
||||
|
||||
k, err := registry.OpenKey(hive, parentPath, registry.WRITE)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer k.Close()
|
||||
|
||||
return registry.DeleteKey(k, keyName)
|
||||
}
|
||||
|
||||
// RunUninstaller executes the uninstall command
|
||||
func RunUninstaller(cmdString string) error {
|
||||
fmt.Printf("RunUninstaller Called with: %s\n", cmdString)
|
||||
cmd := exec.Command("cmd", "/C", cmdString)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: false} // Show window so user can click next
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
fmt.Printf("RunUninstaller Error: %v\n", err)
|
||||
return err
|
||||
}
|
||||
fmt.Printf("RunUninstaller Started Successfully\n")
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package platform
|
||||
|
||||
type SystemInfo struct {
|
||||
Model string `json:"model"`
|
||||
Chip string `json:"chip"`
|
||||
Memory string `json:"memory"`
|
||||
OS string `json:"os"`
|
||||
}
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
//go:build darwin
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func OpenSettings() error {
|
||||
return exec.Command("open", "x-apple.systempreferences:com.apple.settings.Storage").Run()
|
||||
}
|
||||
|
||||
func GetSystemInfo() (*SystemInfo, error) {
|
||||
// Structs for parsing system_profiler JSON
|
||||
type HardwareItem struct {
|
||||
MachineName string `json:"machine_name"`
|
||||
ChipType string `json:"chip_type"`
|
||||
PhysicalMemory string `json:"physical_memory"`
|
||||
}
|
||||
|
||||
type SoftwareItem struct {
|
||||
OSVersion string `json:"os_version"`
|
||||
}
|
||||
|
||||
type SystemProfile struct {
|
||||
Hardware []HardwareItem `json:"SPHardwareDataType"`
|
||||
Software []SoftwareItem `json:"SPSoftwareDataType"`
|
||||
}
|
||||
|
||||
cmd := exec.Command("system_profiler", "SPHardwareDataType", "SPSoftwareDataType", "-json")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var profile SystemProfile
|
||||
if err := json.Unmarshal(output, &profile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := &SystemInfo{
|
||||
Model: "Unknown",
|
||||
Chip: "Unknown",
|
||||
Memory: "Unknown",
|
||||
OS: "Unknown",
|
||||
}
|
||||
|
||||
if len(profile.Hardware) > 0 {
|
||||
info.Model = profile.Hardware[0].MachineName
|
||||
info.Chip = profile.Hardware[0].ChipType
|
||||
info.Memory = profile.Hardware[0].PhysicalMemory
|
||||
}
|
||||
if len(profile.Software) > 0 {
|
||||
info.OS = profile.Software[0].OSVersion
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func EmptyTrash() error {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
trashPath := filepath.Join(home, ".Trash")
|
||||
|
||||
entries, err := os.ReadDir(trashPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
itemPath := filepath.Join(trashPath, entry.Name())
|
||||
os.RemoveAll(itemPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetCachePath() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(home, "Library", "Caches"), nil
|
||||
}
|
||||
|
||||
func GetDockerPath() (string, error) {
|
||||
dockerPath, err := exec.LookPath("docker")
|
||||
if err != nil {
|
||||
// Try common locations
|
||||
commonPaths := []string{
|
||||
"/usr/local/bin/docker",
|
||||
"/opt/homebrew/bin/docker",
|
||||
"/Applications/Docker.app/Contents/Resources/bin/docker",
|
||||
}
|
||||
for _, p := range commonPaths {
|
||||
if _, e := os.Stat(p); e == nil {
|
||||
dockerPath = p
|
||||
return dockerPath, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if dockerPath != "" {
|
||||
return dockerPath, nil
|
||||
}
|
||||
return "", fmt.Errorf("docker not found")
|
||||
}
|
||||
|
||||
func OpenBrowser(url string) error {
|
||||
return exec.Command("open", url).Start()
|
||||
}
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
//go:build windows
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func OpenSettings() error {
|
||||
// Open Windows Settings -> Storage
|
||||
// ms-settings:storagesense
|
||||
return exec.Command("cmd", "/c", "start", "ms-settings:storagesense").Run()
|
||||
}
|
||||
|
||||
func GetSystemInfo() (*SystemInfo, error) {
|
||||
// Use systeminfo or wmic
|
||||
// simpler: generic info
|
||||
|
||||
info := &SystemInfo{
|
||||
Model: "PC",
|
||||
Chip: "Unknown",
|
||||
Memory: "Unknown",
|
||||
OS: "Windows",
|
||||
}
|
||||
|
||||
// Helper to run powershell and get string result
|
||||
runPS := func(cmd string) string {
|
||||
out, err := exec.Command("powershell", "-NoProfile", "-Command", cmd).Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
// 1. Get OS Name (Simplified)
|
||||
// Get-CimInstance Win32_OperatingSystem | Select-Object -ExpandProperty Caption
|
||||
osName := runPS("(Get-CimInstance Win32_OperatingSystem).Caption")
|
||||
if osName != "" {
|
||||
info.OS = strings.TrimPrefix(osName, "Microsoft ")
|
||||
}
|
||||
|
||||
// 2. Get Memory (in GB)
|
||||
// [math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB)
|
||||
mem := runPS("[math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB)")
|
||||
if mem != "" {
|
||||
info.Memory = mem + " GB"
|
||||
}
|
||||
|
||||
// 3. Get CPU Name
|
||||
// (Get-CimInstance Win32_Processor).Name
|
||||
cpu := runPS("(Get-CimInstance Win32_Processor).Name")
|
||||
if cpu != "" {
|
||||
// Cleanup CPU string (remove extra spaces)
|
||||
info.Chip = strings.Join(strings.Fields(cpu), " ")
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func EmptyTrash() error {
|
||||
// PowerShell to empty Recycle Bin
|
||||
// Clear-RecycleBin -Force -ErrorAction SilentlyContinue
|
||||
|
||||
// PowerShell to empty Recycle Bin
|
||||
// Clear-RecycleBin -Force -ErrorAction SilentlyContinue
|
||||
// We use ExecutionPolicy Bypass to avoid permission issues.
|
||||
// We also catch errors to prevent 500s on empty bins.
|
||||
|
||||
cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", "Clear-RecycleBin -Force -ErrorAction SilentlyContinue")
|
||||
// If it returns an error, it might be due to permissions or being already empty.
|
||||
// We can ignore the error for now to check if that fixes the User's 500.
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
// Log it but return nil effectively?
|
||||
// For now, let's return nil because 'Empty Trash' is best-effort.
|
||||
// If the user really has a permission issue, it acts as a no-op which is better than a crash.
|
||||
fmt.Printf("EmptyTrash warning: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetCachePath() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(home, "AppData", "Local", "Temp"), nil
|
||||
}
|
||||
|
||||
func GetDockerPath() (string, error) {
|
||||
path, err := exec.LookPath("docker")
|
||||
if err == nil {
|
||||
return path, nil
|
||||
}
|
||||
// Common Windows path?
|
||||
return "", fmt.Errorf("docker not found")
|
||||
}
|
||||
|
||||
func OpenBrowser(url string) error {
|
||||
return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
|
||||
}
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
//go:build darwin
|
||||
|
||||
package scanner
|
||||
|
||||
import (
|
||||
|
|
@ -12,10 +10,33 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
// Structs moved to scanner_common.go
|
||||
type ScanResult struct {
|
||||
Path string `json:"path"`
|
||||
Size int64 `json:"size"`
|
||||
IsDirectory bool `json:"isDirectory"`
|
||||
}
|
||||
|
||||
type DiskUsage struct {
|
||||
TotalGB string `json:"totalGB"`
|
||||
UsedGB string `json:"usedGB"`
|
||||
FreeGB string `json:"freeGB"`
|
||||
}
|
||||
|
||||
type CategorySizes struct {
|
||||
Documents int64 `json:"documents"` // Personal Docs only
|
||||
Downloads int64 `json:"downloads"`
|
||||
Desktop int64 `json:"desktop"`
|
||||
Music int64 `json:"music"`
|
||||
Movies int64 `json:"movies"`
|
||||
System int64 `json:"system"`
|
||||
Trash int64 `json:"trash"`
|
||||
Apps int64 `json:"apps"`
|
||||
Photos int64 `json:"photos"`
|
||||
ICloud int64 `json:"icloud"`
|
||||
}
|
||||
|
||||
// GetDiskUsage uses diskutil for accurate APFS disk usage
|
||||
func GetDiskUsage() ([]*DiskUsage, error) {
|
||||
func GetDiskUsage() (*DiskUsage, error) {
|
||||
cmd := exec.Command("diskutil", "info", "/")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
|
|
@ -59,15 +80,59 @@ func GetDiskUsage() ([]*DiskUsage, error) {
|
|||
return fmt.Sprintf("%.2f", gb)
|
||||
}
|
||||
|
||||
return []*DiskUsage{{
|
||||
Name: "Macintosh HD",
|
||||
return &DiskUsage{
|
||||
TotalGB: toGB(containerTotal),
|
||||
UsedGB: toGB(containerUsed),
|
||||
FreeGB: toGB(containerFree),
|
||||
}}, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
// FindLargeFiles moved to scanner_common.go
|
||||
// FindLargeFiles walks a directory and returns files > threshold
|
||||
func FindLargeFiles(root string, threshold int64) ([]ScanResult, error) {
|
||||
var results []ScanResult
|
||||
|
||||
err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil // Skip errors
|
||||
}
|
||||
|
||||
// Skip hidden files/dirs (except .Trash maybe, but let's skip all . for now)
|
||||
if strings.HasPrefix(d.Name(), ".") {
|
||||
if d.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip node_modules explicitly
|
||||
if d.IsDir() && d.Name() == "node_modules" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
if !d.IsDir() {
|
||||
info, err := d.Info()
|
||||
if err == nil && info.Size() > threshold {
|
||||
results = append(results, ScanResult{
|
||||
Path: path,
|
||||
Size: info.Size(),
|
||||
IsDirectory: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Sort by size desc
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Size > results[j].Size
|
||||
})
|
||||
|
||||
// Return top 50
|
||||
if len(results) > 50 {
|
||||
return results[:50], err
|
||||
}
|
||||
return results, err
|
||||
}
|
||||
|
||||
// FindHeavyFolders uses `du` to find large directories
|
||||
func FindHeavyFolders(root string) ([]ScanResult, error) {
|
||||
|
|
@ -319,7 +384,10 @@ func GetCategorySizes() (*CategorySizes, error) {
|
|||
return sizes, nil
|
||||
}
|
||||
|
||||
// CleaningEstimates struct moved to scanner_common.go
|
||||
type CleaningEstimates struct {
|
||||
FlashEst int64 `json:"flash_est"`
|
||||
DeepEst int64 `json:"deep_est"`
|
||||
}
|
||||
|
||||
func GetCleaningEstimates() (*CleaningEstimates, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
package scanner
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ScanResult struct {
|
||||
Path string `json:"path"`
|
||||
Size int64 `json:"size"`
|
||||
IsDirectory bool `json:"isDirectory"`
|
||||
}
|
||||
|
||||
type DiskUsage struct {
|
||||
Name string `json:"name"` // e.g. "Local Disk (C:)"
|
||||
TotalGB string `json:"totalGB"`
|
||||
UsedGB string `json:"usedGB"`
|
||||
FreeGB string `json:"freeGB"`
|
||||
}
|
||||
|
||||
type CategorySizes struct {
|
||||
Documents int64 `json:"documents"` // Personal Docs only
|
||||
Downloads int64 `json:"downloads"`
|
||||
Desktop int64 `json:"desktop"`
|
||||
Music int64 `json:"music"`
|
||||
Movies int64 `json:"movies"`
|
||||
System int64 `json:"system"`
|
||||
Trash int64 `json:"trash"`
|
||||
Apps int64 `json:"apps"`
|
||||
Photos int64 `json:"photos"`
|
||||
ICloud int64 `json:"icloud"` // Or OneDrive on Windows?
|
||||
Archives int64 `json:"archives"`
|
||||
VirtualMachines int64 `json:"virtual_machines"`
|
||||
Games int64 `json:"games"`
|
||||
AI int64 `json:"ai"`
|
||||
Docker int64 `json:"docker"`
|
||||
Cache int64 `json:"cache"`
|
||||
}
|
||||
|
||||
type CleaningEstimates struct {
|
||||
FlashEst int64 `json:"flash_est"`
|
||||
DeepEst int64 `json:"deep_est"`
|
||||
}
|
||||
|
||||
// FindLargeFiles walks a directory and returns files > threshold
|
||||
func FindLargeFiles(root string, threshold int64) ([]ScanResult, error) {
|
||||
var results []ScanResult
|
||||
|
||||
err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil // Skip errors
|
||||
}
|
||||
|
||||
// Skip hidden files/dirs (except .Trash maybe, but let's skip all . for now)
|
||||
if strings.HasPrefix(d.Name(), ".") {
|
||||
if d.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip node_modules explicitly
|
||||
if d.IsDir() && d.Name() == "node_modules" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
if !d.IsDir() {
|
||||
info, err := d.Info()
|
||||
if err == nil && info.Size() > threshold {
|
||||
results = append(results, ScanResult{
|
||||
Path: path,
|
||||
Size: info.Size(),
|
||||
IsDirectory: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Sort by size desc
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Size > results[j].Size
|
||||
})
|
||||
|
||||
// Return top 50
|
||||
if len(results) > 50 {
|
||||
return results[:50], err
|
||||
}
|
||||
return results, err
|
||||
}
|
||||
|
|
@ -1,435 +0,0 @@
|
|||
//go:build windows
|
||||
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
// Added missing import
|
||||
)
|
||||
|
||||
// GetDiskUsage using GetDiskFreeSpaceExW
|
||||
// GetDiskUsage returns usage for all fixed drives
|
||||
func GetDiskUsage() ([]*DiskUsage, error) {
|
||||
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
||||
getDiskFreeSpaceEx := kernel32.NewProc("GetDiskFreeSpaceExW")
|
||||
getLogicalDrives := kernel32.NewProc("GetLogicalDrives")
|
||||
|
||||
var usages []*DiskUsage
|
||||
|
||||
// Get logical drives bitmask
|
||||
ret, _, _ := getLogicalDrives.Call()
|
||||
if ret == 0 {
|
||||
return nil, fmt.Errorf("GetLogicalDrives failed")
|
||||
}
|
||||
drivesBitmask := uint32(ret)
|
||||
|
||||
toGB := func(bytes int64) string {
|
||||
gb := float64(bytes) / 1024 / 1024 / 1024
|
||||
return fmt.Sprintf("%.2f", gb)
|
||||
}
|
||||
|
||||
for i := 0; i < 26; i++ {
|
||||
if drivesBitmask&(1<<uint(i)) != 0 {
|
||||
driveLetter := string(rune('A' + i))
|
||||
root := driveLetter + ":\\"
|
||||
|
||||
// Check drive type? strictly speaking GetDiskFreeSpaceEx works on network too.
|
||||
// Ideally check GetDriveType to avoid floppy/cd, but usually no biggie if we just check free space.
|
||||
|
||||
var freeBytesAvailable, totalNumberOfBytes, totalNumberOfFreeBytes int64
|
||||
pathPtr, _ := syscall.UTF16PtrFromString(root)
|
||||
|
||||
r, _, _ := getDiskFreeSpaceEx.Call(
|
||||
uintptr(unsafe.Pointer(pathPtr)),
|
||||
uintptr(unsafe.Pointer(&freeBytesAvailable)),
|
||||
uintptr(unsafe.Pointer(&totalNumberOfBytes)),
|
||||
uintptr(unsafe.Pointer(&totalNumberOfFreeBytes)),
|
||||
)
|
||||
|
||||
if r != 0 && totalNumberOfBytes > 0 {
|
||||
usedBytes := totalNumberOfBytes - totalNumberOfFreeBytes
|
||||
usages = append(usages, &DiskUsage{
|
||||
Name: fmt.Sprintf("Local Disk (%s:)", driveLetter),
|
||||
TotalGB: toGB(totalNumberOfBytes),
|
||||
UsedGB: toGB(usedBytes),
|
||||
FreeGB: toGB(totalNumberOfFreeBytes),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return usages, nil
|
||||
}
|
||||
|
||||
// GetDirectorySize walks the directory to calculate size (Windows doesn't have `du`)
|
||||
func GetDirectorySize(path string) int64 {
|
||||
var size int64
|
||||
filepath.WalkDir(path, func(_ string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if !d.IsDir() {
|
||||
info, err := d.Info()
|
||||
if err == nil {
|
||||
size += info.Size()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return size
|
||||
}
|
||||
|
||||
// FindHeavyFolders finds large directories
|
||||
func FindHeavyFolders(root string) ([]ScanResult, error) {
|
||||
// Basic implementation: Walk max 2 levels deep and calculate sizes
|
||||
var results []ScanResult
|
||||
|
||||
// depth 0 = root
|
||||
// depth 1 = children of root
|
||||
// depth 2 = children of children
|
||||
|
||||
entries, err := os.ReadDir(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
path := filepath.Join(root, entry.Name())
|
||||
wg.Add(1)
|
||||
go func(p string) {
|
||||
defer wg.Done()
|
||||
s := GetDirectorySize(p)
|
||||
mu.Lock()
|
||||
results = append(results, ScanResult{
|
||||
Path: p,
|
||||
Size: s,
|
||||
IsDirectory: true,
|
||||
})
|
||||
mu.Unlock()
|
||||
}(path)
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Sort by size desc
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Size > results[j].Size
|
||||
})
|
||||
|
||||
if len(results) > 50 {
|
||||
return results[:50], nil
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func ScanUserDocuments() ([]ScanResult, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
targets := []string{
|
||||
filepath.Join(home, "Documents"),
|
||||
filepath.Join(home, "Downloads"),
|
||||
filepath.Join(home, "Desktop"),
|
||||
}
|
||||
|
||||
var allResults []ScanResult
|
||||
for _, t := range targets {
|
||||
res, _ := FindLargeFiles(t, 10*1024*1024) // 10MB
|
||||
allResults = append(allResults, res...)
|
||||
}
|
||||
|
||||
sort.Slice(allResults, func(i, j int) bool {
|
||||
return allResults[i].Size > allResults[j].Size
|
||||
})
|
||||
|
||||
if len(allResults) > 50 {
|
||||
return allResults[:50], nil
|
||||
}
|
||||
return allResults, nil
|
||||
}
|
||||
|
||||
func ScanSystemData() ([]ScanResult, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Windows System/Temp locations
|
||||
// %Temp%, Prefetch (admin only, careful), AppData/Local/Temp
|
||||
targets := []string{
|
||||
filepath.Join(home, "AppData", "Local", "Temp"),
|
||||
os.Getenv("TEMP"),
|
||||
// "C:\\Windows\\Temp", // Requires Admin, maybe skip for now or handle error
|
||||
}
|
||||
|
||||
var allResults []ScanResult
|
||||
for _, t := range targets {
|
||||
if t == "" {
|
||||
continue
|
||||
}
|
||||
res, _ := FindLargeFiles(t, 10*1024*1024)
|
||||
allResults = append(allResults, res...)
|
||||
}
|
||||
|
||||
sort.Slice(allResults, func(i, j int) bool {
|
||||
return allResults[i].Size > allResults[j].Size
|
||||
})
|
||||
|
||||
if len(allResults) > 50 {
|
||||
return allResults[:50], nil
|
||||
}
|
||||
return allResults, nil
|
||||
}
|
||||
|
||||
func GetCategorySizes() (*CategorySizes, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
docPath := filepath.Join(home, "Documents")
|
||||
downPath := filepath.Join(home, "Downloads")
|
||||
deskPath := filepath.Join(home, "Desktop")
|
||||
musicPath := filepath.Join(home, "Music")
|
||||
moviesPath := filepath.Join(home, "Videos") // Windows uses Videos
|
||||
photos := filepath.Join(home, "Pictures")
|
||||
|
||||
// AppData is roughly Library
|
||||
localAppData := filepath.Join(home, "AppData", "Local")
|
||||
temp := filepath.Join(localAppData, "Temp")
|
||||
|
||||
// Parallel fetch
|
||||
type result struct {
|
||||
name string
|
||||
size int64
|
||||
}
|
||||
c := make(chan result)
|
||||
// Checks: docs, down, desk, music, movies, temp, photos, archives, vms, games, ai, docker, cache
|
||||
totalChecks := 13
|
||||
|
||||
check := func(name, p string) {
|
||||
c <- result{name, GetDirectorySize(p)}
|
||||
}
|
||||
|
||||
go check("docs", docPath)
|
||||
go check("down", downPath)
|
||||
go check("desk", deskPath)
|
||||
go check("music", musicPath)
|
||||
go check("movies", moviesPath)
|
||||
// Temp is part of Cache now, but let's keep it separate or sum it up
|
||||
// System/Temp logic from before:
|
||||
go check("temp", temp)
|
||||
go check("photos", photos)
|
||||
|
||||
// Scan specific common folders for Archives and VMs
|
||||
go func() {
|
||||
// Archives: Zip, Rar, 7z in Downloads and Documents
|
||||
size := ScanExtensions(downPath, []string{".zip", ".rar", ".7z", ".tar", ".gz", ".xz"})
|
||||
size += ScanExtensions(docPath, []string{".zip", ".rar", ".7z", ".tar", ".gz", ".xz"})
|
||||
c <- result{"archives", size}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
// VMs / Disk Images: ISO, VHDX, VMDK in Downloads and Documents
|
||||
size := ScanExtensions(downPath, []string{".iso", ".vdi", ".vmdk", ".qcow2", ".vhdx", ".img", ".dsk"})
|
||||
size += ScanExtensions(docPath, []string{".iso", ".vdi", ".vmdk", ".qcow2", ".vhdx", ".img", ".dsk"})
|
||||
c <- result{"vms", size}
|
||||
}()
|
||||
|
||||
// Games
|
||||
go func() {
|
||||
var size int64
|
||||
// Common Game Paths
|
||||
paths := []string{
|
||||
`C:\Program Files (x86)\Steam\steamapps\common`,
|
||||
`C:\Program Files\Epic Games`,
|
||||
`C:\Program Files (x86)\Ubisoft\Ubisoft Game Launcher\games`,
|
||||
`C:\Program Files\EA Games`,
|
||||
filepath.Join(home, "AppData", "Roaming", ".minecraft"),
|
||||
}
|
||||
for _, p := range paths {
|
||||
size += GetDirectorySize(p)
|
||||
}
|
||||
c <- result{"games", size}
|
||||
}()
|
||||
|
||||
// AI
|
||||
go func() {
|
||||
var size int64
|
||||
// 1. Common Installation Paths
|
||||
paths := []string{
|
||||
`C:\ComfyUI`,
|
||||
`C:\ai\ComfyUI`,
|
||||
filepath.Join(home, "ComfyUI"),
|
||||
filepath.Join(home, "stable-diffusion-webui"),
|
||||
filepath.Join(home, "webui"),
|
||||
// Common Model Caches
|
||||
filepath.Join(home, ".cache", "huggingface"),
|
||||
filepath.Join(home, ".ollama", "models"),
|
||||
filepath.Join(home, ".lmstudio", "models"),
|
||||
}
|
||||
for _, p := range paths {
|
||||
size += GetDirectorySize(p)
|
||||
}
|
||||
|
||||
// 2. Loose Model Files (Deep Scan)
|
||||
// Look for .safetensors, .ckpt, .gguf, .pt, .pth, .bin, .onnx in Downloads and Documents
|
||||
aiExtensions := []string{".safetensors", ".ckpt", ".gguf", ".pt", ".pth", ".bin", ".onnx"}
|
||||
size += ScanExtensions(downPath, aiExtensions)
|
||||
size += ScanExtensions(docPath, aiExtensions)
|
||||
|
||||
c <- result{"ai", size}
|
||||
}()
|
||||
|
||||
// Docker
|
||||
go func() {
|
||||
var size int64
|
||||
// Docker Desktop Default WSL Data
|
||||
dockerPath := filepath.Join(localAppData, "Docker", "wsl", "data", "ext4.vhdx")
|
||||
info, err := os.Stat(dockerPath)
|
||||
if err == nil {
|
||||
size = info.Size()
|
||||
}
|
||||
c <- result{"docker", size}
|
||||
}()
|
||||
|
||||
// Cache (Browser + System Temp)
|
||||
go func() {
|
||||
var size int64
|
||||
// System Temp
|
||||
size += GetDirectorySize(os.Getenv("TEMP"))
|
||||
|
||||
// Chrome Cache
|
||||
size += GetDirectorySize(filepath.Join(localAppData, "Google", "Chrome", "User Data", "Default", "Cache"))
|
||||
// Edge Cache
|
||||
size += GetDirectorySize(filepath.Join(localAppData, "Microsoft", "Edge", "User Data", "Default", "Cache"))
|
||||
// Brave Cache
|
||||
size += GetDirectorySize(filepath.Join(localAppData, "BraveSoftware", "Brave-Browser", "User Data", "Default", "Cache"))
|
||||
// Opera Cache
|
||||
size += GetDirectorySize(filepath.Join(localAppData, "Opera Software", "Opera Stable", "Cache"))
|
||||
// Firefox Cache
|
||||
size += GetDirectorySize(filepath.Join(localAppData, "Mozilla", "Firefox", "Profiles")) // Scan all profiles for cache? Usually in Local/Mozilla/Firefox/Profiles/<profile>/cache2
|
||||
|
||||
// Firefox requires walking profiles in LocalAppData
|
||||
mozPath := filepath.Join(localAppData, "Mozilla", "Firefox", "Profiles")
|
||||
entries, _ := os.ReadDir(mozPath)
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
size += GetDirectorySize(filepath.Join(mozPath, e.Name(), "cache2"))
|
||||
}
|
||||
}
|
||||
|
||||
c <- result{"cache", size}
|
||||
}()
|
||||
|
||||
sizes := &CategorySizes{}
|
||||
|
||||
for i := 0; i < totalChecks; i++ {
|
||||
res := <-c
|
||||
switch res.name {
|
||||
case "docs":
|
||||
sizes.Documents = res.size
|
||||
case "down":
|
||||
sizes.Downloads = res.size
|
||||
case "desk":
|
||||
sizes.Desktop = res.size
|
||||
case "music":
|
||||
sizes.Music = res.size
|
||||
case "movies":
|
||||
sizes.Movies = res.size
|
||||
case "temp":
|
||||
// Keeping legacy System field for now, maybe map to part of Cache or System logs?
|
||||
sizes.System = res.size
|
||||
case "photos":
|
||||
sizes.Photos = res.size
|
||||
case "archives":
|
||||
sizes.Archives = res.size
|
||||
case "vms":
|
||||
sizes.VirtualMachines = res.size
|
||||
case "games":
|
||||
sizes.Games = res.size
|
||||
case "ai":
|
||||
sizes.AI = res.size
|
||||
case "docker":
|
||||
sizes.Docker = res.size
|
||||
case "cache":
|
||||
sizes.Cache = res.size
|
||||
}
|
||||
}
|
||||
|
||||
return sizes, nil
|
||||
}
|
||||
|
||||
// ScanExtensions walks a directory and sums up size of files with matching extensions
|
||||
func ScanExtensions(root string, exts []string) int64 {
|
||||
var total int64
|
||||
extMap := make(map[string]bool)
|
||||
for _, e := range exts {
|
||||
extMap[strings.ToLower(e)] = true
|
||||
}
|
||||
|
||||
filepath.WalkDir(root, func(_ string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if !d.IsDir() {
|
||||
ext := strings.ToLower(filepath.Ext(d.Name()))
|
||||
if extMap[ext] {
|
||||
info, err := d.Info()
|
||||
if err == nil {
|
||||
total += info.Size()
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return total
|
||||
}
|
||||
|
||||
func GetCleaningEstimates() (*CleaningEstimates, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Flash Clean: Temp files
|
||||
temp := filepath.Join(home, "AppData", "Local", "Temp")
|
||||
|
||||
// Deep Clean: Downloads
|
||||
downloads := filepath.Join(home, "Downloads")
|
||||
|
||||
type result struct {
|
||||
name string
|
||||
size int64
|
||||
}
|
||||
c := make(chan result)
|
||||
|
||||
go func() { c <- result{"temp", GetDirectorySize(temp)} }()
|
||||
go func() { c <- result{"downloads", GetDirectorySize(downloads)} }()
|
||||
|
||||
estimates := &CleaningEstimates{}
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
res := <-c
|
||||
switch res.name {
|
||||
case "temp":
|
||||
estimates.FlashEst = res.size
|
||||
case "downloads":
|
||||
estimates.DeepEst = res.size
|
||||
}
|
||||
}
|
||||
return estimates, nil
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
//go:build darwin
|
||||
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func GetScanTargets(category string) []string {
|
||||
home, _ := os.UserHomeDir()
|
||||
switch category {
|
||||
case "apps":
|
||||
return []string{"/Applications", filepath.Join(home, "Applications")}
|
||||
case "photos":
|
||||
return []string{filepath.Join(home, "Pictures")}
|
||||
case "icloud":
|
||||
return []string{filepath.Join(home, "Library", "Mobile Documents")}
|
||||
case "docs":
|
||||
return []string{filepath.Join(home, "Documents")}
|
||||
case "downloads":
|
||||
return []string{filepath.Join(home, "Downloads")}
|
||||
case "desktop":
|
||||
return []string{filepath.Join(home, "Desktop")}
|
||||
case "music":
|
||||
return []string{filepath.Join(home, "Music")}
|
||||
case "movies":
|
||||
return []string{filepath.Join(home, "Movies")}
|
||||
case "system":
|
||||
return []string{
|
||||
filepath.Join(home, "Library", "Caches"),
|
||||
filepath.Join(home, "Library", "Logs"),
|
||||
filepath.Join(home, "Library", "Developer", "Xcode", "DerivedData"),
|
||||
}
|
||||
case "trash":
|
||||
return []string{filepath.Join(home, ".Trash")}
|
||||
default:
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
package scanner
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func GetScanTargets(category string) []string {
|
||||
home, _ := os.UserHomeDir()
|
||||
switch category {
|
||||
case "apps":
|
||||
// Windows apps are dispersed (Program Files), usually read-only. We don't file-scan them usually.
|
||||
return []string{
|
||||
os.Getenv("ProgramFiles"),
|
||||
os.Getenv("ProgramFiles(x86)"),
|
||||
filepath.Join(os.Getenv("LocalAppData"), "Programs"),
|
||||
}
|
||||
case "photos":
|
||||
return []string{filepath.Join(home, "Pictures")}
|
||||
case "icloud":
|
||||
// iCloudDrive?
|
||||
return []string{filepath.Join(home, "iCloudDrive")}
|
||||
case "docs":
|
||||
return []string{filepath.Join(home, "Documents")}
|
||||
case "downloads":
|
||||
return []string{filepath.Join(home, "Downloads")}
|
||||
case "desktop":
|
||||
return []string{filepath.Join(home, "Desktop")}
|
||||
case "music":
|
||||
return []string{filepath.Join(home, "Music")}
|
||||
case "movies":
|
||||
return []string{filepath.Join(home, "Videos")}
|
||||
case "system":
|
||||
return []string{
|
||||
filepath.Join(home, "AppData", "Local", "Temp"),
|
||||
filepath.Join(home, "AppData", "Local", "Microsoft", "Windows", "INetCache"), // IE/Edge cache
|
||||
filepath.Join(home, "AppData", "Local", "Google", "Chrome", "User Data", "Default", "Cache"),
|
||||
filepath.Join(home, "AppData", "Local", "Mozilla", "Firefox", "Profiles"),
|
||||
filepath.Join(home, "AppData", "Local", "BraveSoftware", "Brave-Browser", "User Data", "Default", "Cache"),
|
||||
filepath.Join(home, "AppData", "Local", "Opera Software", "Opera Stable", "Cache"),
|
||||
}
|
||||
case "cache":
|
||||
return []string{
|
||||
os.Getenv("TEMP"),
|
||||
filepath.Join(home, "AppData", "Local", "Temp"),
|
||||
filepath.Join(home, "AppData", "Local", "Microsoft", "Windows", "INetCache"),
|
||||
filepath.Join(home, "AppData", "Local", "Google", "Chrome", "User Data", "Default", "Cache"),
|
||||
filepath.Join(home, "AppData", "Local", "Mozilla", "Firefox", "Profiles"),
|
||||
filepath.Join(home, "AppData", "Local", "BraveSoftware", "Brave-Browser", "User Data", "Default", "Cache"),
|
||||
filepath.Join(home, "AppData", "Local", "Opera Software", "Opera Stable", "Cache"),
|
||||
}
|
||||
case "games":
|
||||
return []string{
|
||||
`C:\Program Files (x86)\Steam\steamapps\common`,
|
||||
`C:\Program Files\Epic Games`,
|
||||
`C:\Program Files (x86)\Ubisoft\Ubisoft Game Launcher\games`,
|
||||
`C:\Program Files\EA Games`,
|
||||
filepath.Join(home, "AppData", "Roaming", ".minecraft"),
|
||||
}
|
||||
case "ai":
|
||||
return []string{
|
||||
`C:\ComfyUI`,
|
||||
`C:\ai\ComfyUI`,
|
||||
filepath.Join(home, "ComfyUI"),
|
||||
filepath.Join(home, "stable-diffusion-webui"),
|
||||
filepath.Join(home, "webui"),
|
||||
filepath.Join(home, ".cache", "huggingface"),
|
||||
filepath.Join(home, ".ollama", "models"),
|
||||
filepath.Join(home, ".lmstudio", "models"),
|
||||
}
|
||||
case "docker":
|
||||
return []string{
|
||||
filepath.Join(os.Getenv("LocalAppData"), "Docker", "wsl", "data"),
|
||||
}
|
||||
case "archives":
|
||||
// Archives usually scattered, but main ones in Downloads
|
||||
return []string{
|
||||
filepath.Join(home, "Downloads"),
|
||||
filepath.Join(home, "Documents"),
|
||||
}
|
||||
case "vms":
|
||||
return []string{
|
||||
filepath.Join(home, "Downloads"),
|
||||
filepath.Join(home, "Documents"),
|
||||
filepath.Join(home, "VirtualBox VMs"),
|
||||
}
|
||||
default:
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
938
backend/main.go
938
backend/main.go
|
|
@ -1,475 +1,463 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/kv/clearnup/backend/internal/apps"
|
||||
"github.com/kv/clearnup/backend/internal/cleaner"
|
||||
"github.com/kv/clearnup/backend/internal/platform"
|
||||
"github.com/kv/clearnup/backend/internal/scanner"
|
||||
)
|
||||
|
||||
const Port = ":36969"
|
||||
|
||||
func enableCors(w *http.ResponseWriter) {
|
||||
(*w).Header().Set("Access-Control-Allow-Origin", "*")
|
||||
(*w).Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
|
||||
(*w).Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/api/disk-usage", handleDiskUsage)
|
||||
http.HandleFunc("/api/scan/user", handleScanUser)
|
||||
http.HandleFunc("/api/scan/system", handleScanSystem) // Detailed list
|
||||
http.HandleFunc("/api/scan/sizes", handleScanSizes) // Fast summary
|
||||
http.HandleFunc("/api/scan/deepest", handleDeepestScan)
|
||||
|
||||
http.HandleFunc("/api/scan/category", handleScanCategory)
|
||||
http.HandleFunc("/api/purge", handlePurge)
|
||||
http.HandleFunc("/api/empty-trash", handleEmptyTrash)
|
||||
http.HandleFunc("/api/clear-cache", handleClearCache)
|
||||
http.HandleFunc("/api/clean-docker", handleCleanDocker)
|
||||
http.HandleFunc("/api/clean-xcode", handleCleanXcode)
|
||||
http.HandleFunc("/api/clean-homebrew", handleCleanHomebrew)
|
||||
http.HandleFunc("/api/system-info", handleSystemInfo)
|
||||
http.HandleFunc("/api/estimates", handleCleaningEstimates)
|
||||
|
||||
// App Uninstaller
|
||||
http.HandleFunc("/api/apps", handleScanApps)
|
||||
http.HandleFunc("/api/apps/details", handleAppDetails)
|
||||
http.HandleFunc("/api/apps/action", handleAppAction)
|
||||
http.HandleFunc("/api/apps/uninstall", handleAppUninstall)
|
||||
|
||||
// Static File Serving is handled directly by Electron.
|
||||
// Backend only needs to provide API routes.
|
||||
|
||||
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 {
|
||||
fmt.Printf("Server failed: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
type ScanRequest struct {
|
||||
Category string `json:"category"` // "apps", "photos", "icloud", "docs", "system"
|
||||
}
|
||||
|
||||
func handleScanCategory(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
var req ScanRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
targets := scanner.GetScanTargets(req.Category)
|
||||
if len(targets) == 0 {
|
||||
json.NewEncoder(w).Encode([]scanner.ScanResult{})
|
||||
return
|
||||
}
|
||||
|
||||
var allResults []scanner.ScanResult
|
||||
for _, t := range targets {
|
||||
if t == "" {
|
||||
continue
|
||||
}
|
||||
res, _ := scanner.FindLargeFiles(t, 10*1024*1024) // 10MB
|
||||
allResults = append(allResults, res...)
|
||||
}
|
||||
|
||||
// Sort
|
||||
sort.Slice(allResults, func(i, j int) bool {
|
||||
return allResults[i].Size > allResults[j].Size
|
||||
})
|
||||
if len(allResults) > 50 {
|
||||
allResults = allResults[:50]
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(allResults)
|
||||
}
|
||||
|
||||
func handleOpenSettings(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
if err := platform.OpenSettings(); err != nil {
|
||||
fmt.Printf("Failed to open settings: %v\n", err)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func handleDiskUsage(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
usage, err := scanner.GetDiskUsage()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(usage)
|
||||
}
|
||||
|
||||
func handleScanUser(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
files, err := scanner.ScanUserDocuments()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(files)
|
||||
}
|
||||
|
||||
func handleScanSizes(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
sizes, err := scanner.GetCategorySizes()
|
||||
if err != nil {
|
||||
// Log but return empty
|
||||
fmt.Println("Size scan error:", err)
|
||||
json.NewEncoder(w).Encode(map[string]int64{})
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(sizes)
|
||||
}
|
||||
|
||||
func handleScanSystem(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
files, err := scanner.ScanSystemData()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(files)
|
||||
}
|
||||
|
||||
func handleDeepestScan(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
// Default to Documents for now, or parse body for path
|
||||
home, _ := os.UserHomeDir()
|
||||
target := filepath.Join(home, "Documents")
|
||||
|
||||
folders, err := scanner.FindHeavyFolders(target)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(folders)
|
||||
}
|
||||
|
||||
type PurgeRequest struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
func handlePurge(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
var req PurgeRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := cleaner.PurgePath(req.Path); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to purge: %s", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
func handleEmptyTrash(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
if err := platform.EmptyTrash(); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Cannot empty trash: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
func handleClearCache(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
cachePath, err := platform.GetCachePath()
|
||||
if err != nil {
|
||||
http.Error(w, "Cannot get cache path", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get size before clearing
|
||||
sizeBefore := scanner.GetDirectorySize(cachePath)
|
||||
|
||||
// Clear cache directories (keep the Caches folder itself if possible, or jus remove content)
|
||||
entries, err := os.ReadDir(cachePath)
|
||||
if err != nil {
|
||||
http.Error(w, "Cannot read cache directory", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
itemPath := filepath.Join(cachePath, entry.Name())
|
||||
os.RemoveAll(itemPath)
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]int64{"cleared": sizeBefore})
|
||||
}
|
||||
|
||||
func handleCleanDocker(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
dockerPath, err := platform.GetDockerPath()
|
||||
if err != nil {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"cleared": 0,
|
||||
"message": "Docker not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Run docker system prune -af --volumes to clean images, containers, and volumes
|
||||
cmd := exec.Command(dockerPath, "system", "prune", "-af", "--volumes")
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
message := string(output)
|
||||
if message == "" || len(message) > 500 { // fallback if output is empty mapping or huge
|
||||
message = err.Error()
|
||||
}
|
||||
// If the daemon isn't running, provide a helpful message
|
||||
if strings.Contains(message, "connect: no such file or directory") || strings.Contains(message, "Is the docker daemon running") {
|
||||
message = "Docker daemon is not running. Please start Docker to clean it."
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"cleared": 0,
|
||||
"message": message,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"cleared": 1,
|
||||
"message": string(output),
|
||||
})
|
||||
}
|
||||
|
||||
func handleCleanXcode(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"cleared": 0, "message": "Could not find home directory"})
|
||||
return
|
||||
}
|
||||
|
||||
paths := []string{
|
||||
filepath.Join(home, "Library/Developer/Xcode/DerivedData"),
|
||||
filepath.Join(home, "Library/Developer/Xcode/iOS DeviceSupport"),
|
||||
filepath.Join(home, "Library/Developer/Xcode/Archives"),
|
||||
filepath.Join(home, "Library/Caches/com.apple.dt.Xcode"),
|
||||
}
|
||||
|
||||
totalCleared := int64(0)
|
||||
for _, p := range paths {
|
||||
if stat, err := os.Stat(p); err == nil && stat.IsDir() {
|
||||
size := scanner.GetDirectorySize(p)
|
||||
if err := os.RemoveAll(p); err == nil {
|
||||
totalCleared += size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"cleared": totalCleared, "message": "Xcode Caches Cleared"})
|
||||
}
|
||||
|
||||
func handleCleanHomebrew(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command("brew", "cleanup", "--prune=all")
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"cleared": 0,
|
||||
"message": fmt.Sprintf("Brew cleanup failed: %s", string(output)),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"cleared": 1,
|
||||
"message": "Homebrew Cache Cleared",
|
||||
})
|
||||
}
|
||||
|
||||
func handleSystemInfo(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
info, err := platform.GetSystemInfo()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get system info", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(info)
|
||||
}
|
||||
|
||||
func handleCleaningEstimates(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
estimates, err := scanner.GetCleaningEstimates()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(estimates)
|
||||
}
|
||||
|
||||
// App Uninstaller Handlers
|
||||
|
||||
func handleScanApps(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
appsList, err := apps.ScanApps()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(appsList)
|
||||
}
|
||||
|
||||
func handleAppDetails(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
type AppDetailsRequest struct {
|
||||
Path string `json:"path"`
|
||||
BundleID string `json:"bundleID"`
|
||||
}
|
||||
var req AppDetailsRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
details, err := apps.GetAppDetails(req.Path, req.BundleID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(details)
|
||||
}
|
||||
|
||||
func handleAppAction(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Files []string `json:"files"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := apps.DeleteFiles(req.Files); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to delete files: %s", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
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})
|
||||
}
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/kv/clearnup/backend/internal/apps"
|
||||
"github.com/kv/clearnup/backend/internal/cleaner"
|
||||
"github.com/kv/clearnup/backend/internal/scanner"
|
||||
)
|
||||
|
||||
const Port = ":36969"
|
||||
|
||||
func enableCors(w *http.ResponseWriter) {
|
||||
(*w).Header().Set("Access-Control-Allow-Origin", "*")
|
||||
(*w).Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
|
||||
(*w).Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/api/disk-usage", handleDiskUsage)
|
||||
http.HandleFunc("/api/scan/user", handleScanUser)
|
||||
http.HandleFunc("/api/scan/system", handleScanSystem) // Detailed list
|
||||
http.HandleFunc("/api/scan/sizes", handleScanSizes) // Fast summary
|
||||
http.HandleFunc("/api/scan/deepest", handleDeepestScan)
|
||||
|
||||
http.HandleFunc("/api/scan/category", handleScanCategory)
|
||||
http.HandleFunc("/api/purge", handlePurge)
|
||||
http.HandleFunc("/api/empty-trash", handleEmptyTrash)
|
||||
http.HandleFunc("/api/clear-cache", handleClearCache)
|
||||
http.HandleFunc("/api/clean-docker", handleCleanDocker)
|
||||
http.HandleFunc("/api/system-info", handleSystemInfo)
|
||||
http.HandleFunc("/api/estimates", handleCleaningEstimates)
|
||||
|
||||
// App Uninstaller
|
||||
http.HandleFunc("/api/apps", handleScanApps)
|
||||
http.HandleFunc("/api/apps/details", handleAppDetails)
|
||||
http.HandleFunc("/api/apps/action", handleAppAction)
|
||||
|
||||
fmt.Printf("🚀 Antigravity Backend running on http://localhost%s\n", Port)
|
||||
if err := http.ListenAndServe(Port, nil); err != nil {
|
||||
fmt.Printf("Server failed: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
type ScanRequest struct {
|
||||
Category string `json:"category"` // "apps", "photos", "icloud", "docs", "system"
|
||||
}
|
||||
|
||||
func handleScanCategory(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
var req ScanRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
home, _ := os.UserHomeDir()
|
||||
var targets []string
|
||||
|
||||
switch req.Category {
|
||||
case "apps":
|
||||
targets = []string{"/Applications", filepath.Join(home, "Applications")}
|
||||
case "photos":
|
||||
targets = []string{filepath.Join(home, "Pictures")}
|
||||
case "icloud":
|
||||
targets = []string{filepath.Join(home, "Library", "Mobile Documents")}
|
||||
case "docs":
|
||||
targets = []string{filepath.Join(home, "Documents")}
|
||||
case "downloads":
|
||||
targets = []string{filepath.Join(home, "Downloads")}
|
||||
case "desktop":
|
||||
targets = []string{filepath.Join(home, "Desktop")}
|
||||
case "music":
|
||||
targets = []string{filepath.Join(home, "Music")}
|
||||
case "movies":
|
||||
targets = []string{filepath.Join(home, "Movies")}
|
||||
case "system":
|
||||
targets = []string{filepath.Join(home, "Library", "Caches"), filepath.Join(home, "Library", "Logs"), filepath.Join(home, "Library", "Developer", "Xcode", "DerivedData")}
|
||||
default:
|
||||
json.NewEncoder(w).Encode([]scanner.ScanResult{})
|
||||
return
|
||||
}
|
||||
|
||||
// Reuse ScanPath logic inline or call a helper
|
||||
// We'll just do a quick loop here since ScanPath in scanner.go was defined but I need to link it
|
||||
// Actually I put ScanPath in scanner.go as FindLargeFiles wrapper.
|
||||
var allResults []scanner.ScanResult
|
||||
for _, t := range targets {
|
||||
res, _ := scanner.FindLargeFiles(t, 10*1024*1024) // 10MB
|
||||
allResults = append(allResults, res...)
|
||||
}
|
||||
|
||||
// Sort
|
||||
sort.Slice(allResults, func(i, j int) bool {
|
||||
return allResults[i].Size > allResults[j].Size
|
||||
})
|
||||
if len(allResults) > 50 {
|
||||
allResults = allResults[:50]
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(allResults)
|
||||
}
|
||||
|
||||
func handleOpenSettings(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
// Open Storage Settings
|
||||
// macOS Ventura+: open x-apple.systempreferences:com.apple.settings.Storage
|
||||
exec.Command("open", "x-apple.systempreferences:com.apple.settings.Storage").Run()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func handleDiskUsage(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
usage, err := scanner.GetDiskUsage()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(usage)
|
||||
}
|
||||
|
||||
func handleScanUser(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
files, err := scanner.ScanUserDocuments()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(files)
|
||||
}
|
||||
|
||||
func handleScanSizes(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
sizes, err := scanner.GetCategorySizes()
|
||||
if err != nil {
|
||||
// Log but return empty
|
||||
fmt.Println("Size scan error:", err)
|
||||
json.NewEncoder(w).Encode(map[string]int64{})
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(sizes)
|
||||
}
|
||||
|
||||
func handleScanSystem(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
files, err := scanner.ScanSystemData()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(files)
|
||||
}
|
||||
|
||||
func handleDeepestScan(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
// Default to Documents for now, or parse body for path
|
||||
home, _ := os.UserHomeDir()
|
||||
target := filepath.Join(home, "Documents")
|
||||
|
||||
folders, err := scanner.FindHeavyFolders(target)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(folders)
|
||||
}
|
||||
|
||||
type PurgeRequest struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
func handlePurge(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
var req PurgeRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := cleaner.PurgePath(req.Path); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to purge: %s", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
func handleEmptyTrash(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
http.Error(w, "Cannot get home directory", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
trashPath := filepath.Join(home, ".Trash")
|
||||
|
||||
// Get all items in trash and delete them
|
||||
entries, err := os.ReadDir(trashPath)
|
||||
if err != nil {
|
||||
http.Error(w, "Cannot read trash", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
itemPath := filepath.Join(trashPath, entry.Name())
|
||||
os.RemoveAll(itemPath)
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
func handleClearCache(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
home, _ := os.UserHomeDir()
|
||||
cachePath := filepath.Join(home, "Library", "Caches")
|
||||
|
||||
// Get size before clearing
|
||||
sizeBefore := scanner.GetDirectorySize(cachePath)
|
||||
|
||||
// Clear cache directories (keep the Caches folder itself)
|
||||
entries, err := os.ReadDir(cachePath)
|
||||
if err != nil {
|
||||
http.Error(w, "Cannot read cache directory", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
itemPath := filepath.Join(cachePath, entry.Name())
|
||||
os.RemoveAll(itemPath)
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]int64{"cleared": sizeBefore})
|
||||
}
|
||||
|
||||
func handleCleanDocker(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
// Try to find docker executable
|
||||
dockerPath, err := exec.LookPath("docker")
|
||||
if err != nil {
|
||||
// Try common locations
|
||||
commonPaths := []string{
|
||||
"/usr/local/bin/docker",
|
||||
"/opt/homebrew/bin/docker",
|
||||
"/Applications/Docker.app/Contents/Resources/bin/docker",
|
||||
}
|
||||
for _, p := range commonPaths {
|
||||
if _, e := os.Stat(p); e == nil {
|
||||
dockerPath = p
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if dockerPath == "" {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"cleared": 0,
|
||||
"message": "Docker not found in PATH or common locations",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Run docker system prune -af
|
||||
cmd := exec.Command(dockerPath, "system", "prune", "-af")
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"cleared": 0,
|
||||
"message": fmt.Sprintf("Docker cleanup failed: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"cleared": 1,
|
||||
"message": string(output),
|
||||
})
|
||||
}
|
||||
|
||||
func handleSystemInfo(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
// Structs for parsing system_profiler JSON
|
||||
type HardwareItem struct {
|
||||
MachineName string `json:"machine_name"`
|
||||
ChipType string `json:"chip_type"`
|
||||
PhysicalMemory string `json:"physical_memory"`
|
||||
}
|
||||
|
||||
type SoftwareItem struct {
|
||||
OSVersion string `json:"os_version"`
|
||||
}
|
||||
|
||||
type SystemProfile struct {
|
||||
Hardware []HardwareItem `json:"SPHardwareDataType"`
|
||||
Software []SoftwareItem `json:"SPSoftwareDataType"`
|
||||
}
|
||||
|
||||
cmd := exec.Command("system_profiler", "SPHardwareDataType", "SPSoftwareDataType", "-json")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get system info", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var profile SystemProfile
|
||||
if err := json.Unmarshal(output, &profile); err != nil {
|
||||
http.Error(w, "Failed to parse system info", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]string{
|
||||
"model": "Unknown",
|
||||
"chip": "Unknown",
|
||||
"memory": "Unknown",
|
||||
"os": "Unknown",
|
||||
}
|
||||
|
||||
if len(profile.Hardware) > 0 {
|
||||
response["model"] = profile.Hardware[0].MachineName
|
||||
response["chip"] = profile.Hardware[0].ChipType
|
||||
response["memory"] = profile.Hardware[0].PhysicalMemory
|
||||
}
|
||||
if len(profile.Software) > 0 {
|
||||
response["os"] = profile.Software[0].OSVersion
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func handleCleaningEstimates(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
estimates, err := scanner.GetCleaningEstimates()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(estimates)
|
||||
}
|
||||
|
||||
// App Uninstaller Handlers
|
||||
|
||||
func handleScanApps(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
appsList, err := apps.ScanApps()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(appsList)
|
||||
}
|
||||
|
||||
type AppDetailsRequest struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
func handleAppDetails(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
var req AppDetailsRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
details, err := apps.GetAppDetails(req.Path)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(details)
|
||||
}
|
||||
|
||||
type AppActionRequest struct {
|
||||
Files []string `json:"files"`
|
||||
}
|
||||
|
||||
func handleAppAction(w http.ResponseWriter, r *http.Request) {
|
||||
enableCors(&w)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
var req AppActionRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := apps.DeleteFiles(req.Files); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to delete files: %s", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]bool{"success": true})
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,25 +0,0 @@
|
|||
$p = Start-Process -FilePath ".\kv-cleanup.exe" -PassThru -NoNewWindow
|
||||
Start-Sleep -Seconds 3
|
||||
|
||||
try {
|
||||
Write-Host "`n=== Disk Usage ==="
|
||||
$disk = Invoke-RestMethod -Uri "http://localhost:36969/api/disk-usage"
|
||||
Write-Host "Total: $($disk.totalGB) GB, Free: $($disk.freeGB) GB"
|
||||
|
||||
Write-Host "`n=== System Info ==="
|
||||
$sys = Invoke-RestMethod -Uri "http://localhost:36969/api/system-info"
|
||||
Write-Host "OS: $($sys.os)"
|
||||
Write-Host "Memory: $($sys.memory)"
|
||||
|
||||
Write-Host "`n=== Apps (First 3) ==="
|
||||
$apps = Invoke-RestMethod -Uri "http://localhost:36969/api/apps"
|
||||
$apps | Select-Object -First 3 | Format-Table Name, Path
|
||||
|
||||
Write-Host "`n=== Scan Downloads ==="
|
||||
$scan = Invoke-RestMethod -Uri "http://localhost:36969/api/scan/category" -Method Post -Body '{"category": "downloads"}' -ContentType "application/json"
|
||||
$scan | Select-Object -First 3 | Format-Table Path, Size
|
||||
} catch {
|
||||
Write-Host "Error: $_"
|
||||
} finally {
|
||||
Stop-Process -Id $p.Id -Force
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
# build-release.ps1
|
||||
# Builds a portable SINGLE-FILE release for Windows and Mac
|
||||
|
||||
Write-Host "Starting Portable Release Build..." -ForegroundColor Cyan
|
||||
|
||||
# 1. Clean previous build
|
||||
if (Test-Path "Release") { Remove-Item "Release" -Recurse -Force }
|
||||
if (Test-Path "backend\dist") { Remove-Item "backend\dist" -Recurse -Force }
|
||||
New-Item -ItemType Directory -Force -Path "Release" | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path "Release\Windows" | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path "Release\Mac" | Out-Null
|
||||
|
||||
# 2. Build Frontend
|
||||
Write-Host "Building Frontend (Vite)..." -ForegroundColor Yellow
|
||||
$pkgManager = "pnpm"
|
||||
if (-not (Get-Command "pnpm" -ErrorAction SilentlyContinue)) { $pkgManager = "npm" }
|
||||
|
||||
Invoke-Expression "$pkgManager install"
|
||||
Invoke-Expression "$pkgManager run build"
|
||||
|
||||
if (-not (Test-Path "dist")) {
|
||||
Write-Host "Frontend build failed: 'dist' folder not found." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 3. Move dist to backend/dist (for embedding)
|
||||
Write-Host "Moving frontend to backend for embedding..." -ForegroundColor Cyan
|
||||
Copy-Item -Path "dist" -Destination "backend\dist" -Recurse
|
||||
|
||||
# 4. Build Backend
|
||||
Write-Host "Building Backend..." -ForegroundColor Yellow
|
||||
|
||||
# Windows Build
|
||||
Write-Host " Windows (amd64)..." -ForegroundColor Cyan
|
||||
$env:GOOS = "windows"; $env:GOARCH = "amd64"
|
||||
go build -ldflags "-s -w -H=windowsgui" -o "Release\Windows\Antigravity.exe" backend/main.go
|
||||
|
||||
# Mac Build (Cross-compile)
|
||||
Write-Host " macOS (amd64 & arm64)..." -ForegroundColor Cyan
|
||||
$env:GOOS = "darwin"; $env:GOARCH = "amd64"
|
||||
go build -ldflags "-s -w" -o "Release\Mac\Antigravity-Intel" backend/main.go
|
||||
|
||||
$env:GOARCH = "arm64"
|
||||
go build -ldflags "-s -w" -o "Release\Mac\Antigravity-AppleSilicon" backend/main.go
|
||||
|
||||
# Cleanup backend/dist
|
||||
Remove-Item "backend\dist" -Recurse -Force
|
||||
|
||||
# 5. Success Message & Zipping
|
||||
Write-Host "Build Complete!" -ForegroundColor Green
|
||||
|
||||
# Zip Windows
|
||||
if (Test-Path "Release\Antigravity-Windows.zip") { Remove-Item "Release\Antigravity-Windows.zip" }
|
||||
Compress-Archive -Path "Release\Windows\*" -DestinationPath "Release\Antigravity-Windows.zip" -Force
|
||||
Write-Host "Created Windows Zip: Release\Antigravity-Windows.zip" -ForegroundColor Green
|
||||
|
||||
# Zip Mac
|
||||
if (Test-Path "Release\Antigravity-Mac.zip") { Remove-Item "Release\Antigravity-Mac.zip" }
|
||||
Compress-Archive -Path "Release\Mac\*" -DestinationPath "Release\Antigravity-Mac.zip" -Force
|
||||
Write-Host "Created Mac Zip: Release\Antigravity-Mac.zip" -ForegroundColor Green
|
||||
|
||||
Write-Host "Artifacts are in the 'Release' folder." -ForegroundColor White
|
||||
|
|
@ -24,6 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|||
|
||||
// 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");
|
||||
|
||||
|
|
@ -292,8 +293,7 @@ var startBackend = () => {
|
|||
console.log("Development mode: Backend should be running via start-go.sh");
|
||||
return;
|
||||
}
|
||||
const backendExec = process.platform === "win32" ? "backend.exe" : "backend";
|
||||
const backendPath = import_path3.default.join(process.resourcesPath, backendExec);
|
||||
const backendPath = import_path3.default.join(process.resourcesPath, "backend");
|
||||
console.log("Starting backend from:", backendPath);
|
||||
try {
|
||||
backendProcess = (0, import_child_process3.spawn)(backendPath, [], {
|
||||
|
|
@ -312,12 +312,11 @@ var startBackend = () => {
|
|||
function createTray() {
|
||||
const iconPath = import_path3.default.join(__dirname, "../dist/tray/tray-iconTemplate.png");
|
||||
let finalIconPath = iconPath;
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
if (!import_fs.default.existsSync(iconPath)) {
|
||||
finalIconPath = import_path3.default.join(__dirname, "../public/tray/tray-iconTemplate.png");
|
||||
}
|
||||
let image = import_electron.nativeImage.createFromPath(finalIconPath);
|
||||
image.setTemplateImage(true);
|
||||
tray = new import_electron.Tray(image.resize({ width: 18, height: 18 }));
|
||||
const image = import_electron.nativeImage.createFromPath(finalIconPath);
|
||||
tray = new import_electron.Tray(image.resize({ width: 16, height: 16 }));
|
||||
tray.setToolTip("Antigravity Cleaner");
|
||||
updateTrayMenu("Initializing...");
|
||||
}
|
||||
|
|
|
|||
0
electron/features/cleaner.ts
Normal file → Executable file
0
electron/features/cleaner.ts
Normal file → Executable file
0
electron/features/enforcer.ts
Normal file → Executable file
0
electron/features/enforcer.ts
Normal file → Executable file
0
electron/features/scanner.ts
Normal file → Executable file
0
electron/features/scanner.ts
Normal file → Executable file
0
electron/features/updater.ts
Normal file → Executable file
0
electron/features/updater.ts
Normal file → Executable file
11
electron/main.ts
Normal file → Executable file
11
electron/main.ts
Normal file → Executable file
|
|
@ -22,8 +22,7 @@ const startBackend = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const backendExec = process.platform === 'win32' ? 'backend.exe' : 'backend';
|
||||
const backendPath = path.join(process.resourcesPath, backendExec);
|
||||
const backendPath = path.join(process.resourcesPath, 'backend');
|
||||
console.log('Starting backend from:', backendPath);
|
||||
|
||||
try {
|
||||
|
|
@ -50,14 +49,12 @@ function createTray() {
|
|||
|
||||
// Check if dist/tray exists, if not try public/tray (dev mode)
|
||||
let finalIconPath = iconPath;
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (!fs.existsSync(iconPath)) {
|
||||
finalIconPath = path.join(__dirname, '../public/tray/tray-iconTemplate.png');
|
||||
}
|
||||
|
||||
let image = nativeImage.createFromPath(finalIconPath);
|
||||
image.setTemplateImage(true);
|
||||
|
||||
tray = new Tray(image.resize({ width: 18, height: 18 }));
|
||||
const image = nativeImage.createFromPath(finalIconPath);
|
||||
tray = new Tray(image.resize({ width: 16, height: 16 }));
|
||||
|
||||
tray.setToolTip('Antigravity Cleaner');
|
||||
updateTrayMenu('Initializing...');
|
||||
|
|
|
|||
0
electron/preload.ts
Normal file → Executable file
0
electron/preload.ts
Normal file → Executable file
|
|
@ -1,20 +0,0 @@
|
|||
const { app, nativeImage } = require('electron');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
app.whenReady().then(() => {
|
||||
const iconPath = path.join(__dirname, '../../build/icon.png');
|
||||
const image = nativeImage.createFromPath(iconPath);
|
||||
|
||||
const resized = image.resize({ width: 22, height: 22, quality: 'best' });
|
||||
|
||||
const pngPath = path.join(__dirname, '../../public/tray/tray-icon.png');
|
||||
const pngPath2 = path.join(__dirname, '../../public/tray/tray-iconTemplate.png');
|
||||
|
||||
const pngBuffer = resized.toPNG();
|
||||
fs.writeFileSync(pngPath, pngBuffer);
|
||||
fs.writeFileSync(pngPath2, pngBuffer);
|
||||
|
||||
console.log('Saved resized built icon to', pngPath);
|
||||
app.quit();
|
||||
});
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
const { app, nativeImage } = require('electron');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
app.whenReady().then(() => {
|
||||
const svgBuffer = fs.readFileSync('/tmp/tray-iconTemplate.svg');
|
||||
const image = nativeImage.createFromBuffer(svgBuffer, { scaleFactor: 2.0 });
|
||||
|
||||
const pngPath = path.join(__dirname, '../../public/tray/tray-iconTemplate.png');
|
||||
const pngPath2 = path.join(__dirname, '../../public/tray/tray-icon.png');
|
||||
|
||||
const pngBuffer = image.toPNG();
|
||||
fs.writeFileSync(pngPath, pngBuffer);
|
||||
fs.writeFileSync(pngPath2, pngBuffer);
|
||||
|
||||
console.log('Saved transparent PNG template to', pngPath);
|
||||
app.quit();
|
||||
});
|
||||
0
electron/tsconfig.json
Normal file → Executable file
0
electron/tsconfig.json
Normal file → Executable file
0
eslint.config.js
Normal file → Executable file
0
eslint.config.js
Normal file → Executable file
8
go.mod
8
go.mod
|
|
@ -1,5 +1,3 @@
|
|||
module github.com/kv/clearnup
|
||||
|
||||
go 1.25.4
|
||||
|
||||
require golang.org/x/sys v0.40.0 // indirect
|
||||
module github.com/kv/clearnup
|
||||
|
||||
go 1.25.4
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -1,2 +0,0 @@
|
|||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
0
index.html
Normal file → Executable file
0
index.html
Normal file → Executable file
174
package.json
Normal file → Executable file
174
package.json
Normal file → Executable file
|
|
@ -1,95 +1,81 @@
|
|||
{
|
||||
"name": "lumina",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "dist-electron/main.cjs",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev:electron": "node scripts/build-electron.mjs && concurrently -k \"vite\" \"wait-on tcp:5173 && cross-env NODE_ENV=development electron dist-electron/main.cjs\"",
|
||||
"electron:build": "node scripts/build-electron.mjs",
|
||||
"build": "tsc -b && vite build",
|
||||
"build:go:mac": "sh scripts/build-go.sh",
|
||||
"build:go:win": "GOOS=windows GOARCH=amd64 go build -ldflags=\"-s -w\" -o backend/dist/windows/backend.exe backend/main.go",
|
||||
"build:mac": "pnpm run build:go:mac && pnpm run build && pnpm run electron:build && electron-builder --mac --universal",
|
||||
"build:win": "pnpm run build:go:win && pnpm run build && pnpm run electron:build && electron-builder --win portable --x64",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"preinstall": "node scripts/check-pnpm.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.29.2",
|
||||
"lucide-react": "^0.563.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"tailwind-merge": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.5",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"concurrently": "^9.1.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"electron": "^33.2.1",
|
||||
"electron-builder": "^26.4.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.3.3",
|
||||
"typescript-eslint": "^8.46.4",
|
||||
"vite": "^7.2.4",
|
||||
"wait-on": "^8.0.1"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"electron",
|
||||
"esbuild"
|
||||
]
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.kv.clearnup",
|
||||
"productName": "KV Clearnup",
|
||||
"directories": {
|
||||
"output": "release"
|
||||
},
|
||||
"compression": "maximum",
|
||||
"mac": {
|
||||
"target": [
|
||||
"dmg"
|
||||
],
|
||||
"icon": "build/icon.png",
|
||||
"category": "public.app-category.utilities",
|
||||
"hardenedRuntime": false,
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "backend/dist/universal/backend",
|
||||
"to": "backend"
|
||||
}
|
||||
]
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
"portable"
|
||||
],
|
||||
"icon": "build/icon.png",
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "backend/dist/windows/backend.exe",
|
||||
"to": "backend.exe"
|
||||
}
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"dist-electron/**/*",
|
||||
"package.json"
|
||||
]
|
||||
}
|
||||
{
|
||||
"name": "Lumina",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"main": "dist-electron/main.cjs",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev:electron": "node scripts/build-electron.mjs && concurrently -k \"vite\" \"wait-on tcp:5173 && cross-env NODE_ENV=development electron dist-electron/main.cjs\"",
|
||||
"electron:build": "node scripts/build-electron.mjs",
|
||||
"build": "tsc -b && vite build",
|
||||
"build:go:mac": "sh scripts/build-go.sh",
|
||||
"build:mac": "npm run build:go:mac && npm run build && npm run electron:build && electron-builder --mac --universal",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"preinstall": "node scripts/check-pnpm.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.29.2",
|
||||
"lucide-react": "^0.563.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"tailwind-merge": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.5",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"concurrently": "^9.1.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"electron": "^33.2.1",
|
||||
"electron-builder": "^26.4.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.46.4",
|
||||
"vite": "^7.2.4",
|
||||
"wait-on": "^8.0.1"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"electron",
|
||||
"esbuild"
|
||||
]
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.kv.clearnup",
|
||||
"productName": "KV Clearnup",
|
||||
"directories": {
|
||||
"output": "release"
|
||||
},
|
||||
"compression": "maximum",
|
||||
"mac": {
|
||||
"target": [
|
||||
"dmg"
|
||||
],
|
||||
"icon": "build/icon.png",
|
||||
"category": "public.app-category.utilities",
|
||||
"hardenedRuntime": false
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"dist-electron/**/*",
|
||||
"package.json"
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "backend/dist/universal/backend",
|
||||
"to": "backend"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
8559
pnpm-lock.yaml
Normal file → Executable file
8559
pnpm-lock.yaml
Normal file → Executable file
File diff suppressed because it is too large
Load diff
0
postcss.config.js
Normal file → Executable file
0
postcss.config.js
Normal file → Executable file
Binary file not shown.
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 513 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 431 KiB |
0
public/vite.svg
Normal file → Executable file
0
public/vite.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
BIN
release/.icon-icns/icon.icns
Normal file
BIN
release/.icon-icns/icon.icns
Normal file
Binary file not shown.
BIN
release/KV Clearnup-0.0.0-universal.dmg
Normal file
BIN
release/KV Clearnup-0.0.0-universal.dmg
Normal file
Binary file not shown.
BIN
release/KV Clearnup-0.0.0-universal.dmg.blockmap
Normal file
BIN
release/KV Clearnup-0.0.0-universal.dmg.blockmap
Normal file
Binary file not shown.
40
release/builder-debug.yml
Normal file
40
release/builder-debug.yml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
x64:
|
||||
firstOrDefaultFilePatterns:
|
||||
- '!**/node_modules/**'
|
||||
- '!build{,/**/*}'
|
||||
- '!release{,/**/*}'
|
||||
- dist/**/*
|
||||
- dist-electron/**/*
|
||||
- package.json
|
||||
- '!**/*.{iml,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,suo,xproj,cc,d.ts,mk,a,o,obj,forge-meta,pdb}'
|
||||
- '!**/._*'
|
||||
- '!**/electron-builder.{yaml,yml,json,json5,toml,ts}'
|
||||
- '!**/{.git,.hg,.svn,CVS,RCS,SCCS,__pycache__,.DS_Store,thumbs.db,.gitignore,.gitkeep,.gitattributes,.npmignore,.idea,.vs,.flowconfig,.jshintrc,.eslintrc,.circleci,.yarn-integrity,.yarn-metadata.json,yarn-error.log,yarn.lock,package-lock.json,npm-debug.log,pnpm-lock.yaml,bun.lock,bun.lockb,appveyor.yml,.travis.yml,circle.yml,.nyc_output,.husky,.github,electron-builder.env}'
|
||||
- '!.yarn{,/**/*}'
|
||||
- '!.editorconfig'
|
||||
- '!.yarnrc.yml'
|
||||
nodeModuleFilePatterns:
|
||||
- '**/*'
|
||||
- dist/**/*
|
||||
- dist-electron/**/*
|
||||
- package.json
|
||||
arm64:
|
||||
firstOrDefaultFilePatterns:
|
||||
- '!**/node_modules/**'
|
||||
- '!build{,/**/*}'
|
||||
- '!release{,/**/*}'
|
||||
- dist/**/*
|
||||
- dist-electron/**/*
|
||||
- package.json
|
||||
- '!**/*.{iml,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,suo,xproj,cc,d.ts,mk,a,o,obj,forge-meta,pdb}'
|
||||
- '!**/._*'
|
||||
- '!**/electron-builder.{yaml,yml,json,json5,toml,ts}'
|
||||
- '!**/{.git,.hg,.svn,CVS,RCS,SCCS,__pycache__,.DS_Store,thumbs.db,.gitignore,.gitkeep,.gitattributes,.npmignore,.idea,.vs,.flowconfig,.jshintrc,.eslintrc,.circleci,.yarn-integrity,.yarn-metadata.json,yarn-error.log,yarn.lock,package-lock.json,npm-debug.log,pnpm-lock.yaml,bun.lock,bun.lockb,appveyor.yml,.travis.yml,circle.yml,.nyc_output,.husky,.github,electron-builder.env}'
|
||||
- '!.yarn{,/**/*}'
|
||||
- '!.editorconfig'
|
||||
- '!.yarnrc.yml'
|
||||
nodeModuleFilePatterns:
|
||||
- '**/*'
|
||||
- dist/**/*
|
||||
- dist-electron/**/*
|
||||
- package.json
|
||||
21
release/builder-effective-config.yaml
Normal file
21
release/builder-effective-config.yaml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
directories:
|
||||
output: release
|
||||
buildResources: build
|
||||
appId: com.kv.clearnup
|
||||
productName: KV Clearnup
|
||||
compression: maximum
|
||||
mac:
|
||||
target:
|
||||
- dmg
|
||||
icon: build/icon.png
|
||||
category: public.app-category.utilities
|
||||
hardenedRuntime: false
|
||||
files:
|
||||
- filter:
|
||||
- dist/**/*
|
||||
- dist-electron/**/*
|
||||
- package.json
|
||||
extraResources:
|
||||
- from: backend/dist/universal/backend
|
||||
to: backend
|
||||
electronVersion: 33.4.11
|
||||
|
|
@ -0,0 +1 @@
|
|||
Versions/Current/Electron Framework
|
||||
|
|
@ -0,0 +1 @@
|
|||
Versions/Current/Helpers
|
||||
|
|
@ -0,0 +1 @@
|
|||
Versions/Current/Libraries
|
||||
|
|
@ -0,0 +1 @@
|
|||
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.
|
|
@ -0,0 +1 @@
|
|||
{"file_format_version": "1.0.0", "ICD": {"library_path": "./libvk_swiftshader.dylib", "api_version": "1.0.5"}}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Electron Framework</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.github.Electron.framework</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Electron Framework</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>33.4.11</string>
|
||||
<key>DTCompiler</key>
|
||||
<string>com.apple.compilers.llvm.clang.1_0</string>
|
||||
<key>DTSDKBuild</key>
|
||||
<string>23F73</string>
|
||||
<key>DTSDKName</key>
|
||||
<string>macosx14.5</string>
|
||||
<key>DTXcode</key>
|
||||
<string>1540</string>
|
||||
<key>DTXcodeBuild</key>
|
||||
<string>15F31d</string>
|
||||
<key>LSEnvironment</key>
|
||||
<dict>
|
||||
<key>MallocNanoZone</key>
|
||||
<string>0</string>
|
||||
</dict>
|
||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||
<true/>
|
||||
<key>ElectronAsarIntegrity</key>
|
||||
<dict>
|
||||
<key>Resources/app.asar</key>
|
||||
<dict>
|
||||
<key>algorithm</key>
|
||||
<string>SHA256</string>
|
||||
<key>hash</key>
|
||||
<string>7f0ca3c6fae4ccfe2d088e243546c0f695b844fbf714bd59e8c6111fb873f334</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
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