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

|

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