Compare commits

...

3 commits
v1.1 ... main

34 changed files with 9856 additions and 9263 deletions

View file

@ -5,22 +5,24 @@ A modern, high-performance system optimizer for macOS, built with **Electron**,
![App Screenshot](https://via.placeholder.com/800x500?text=Antigravity+Dashboard) ![App Screenshot](https://via.placeholder.com/800x500?text=Antigravity+Dashboard)
## 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.
- **App Uninstaller**: View installed applications, their sizes, and thoroughly remove them along with their associated preference files and caches.
- **Deep Clean**: Scan for large files and heavy folders. - **Deep Clean**: Scan for large files and heavy folders.
- **Real-time Monitoring**: Track disk usage and category sizes. - **Real-time Monitoring**: Track disk usage and category sizes.
- **Universal Binary**: Runs natively on both Apple Silicon (M1/M2/M3) and Intel Macs. - **Native Menubar Integration**: Includes a responsive, monochrome template icon that adapts to macOS light/dark modes perfectly.
- **High Performance**: Heavy lifting is handled by a compiled Go backend. - **Cross-Platform**: Runs natively with compiled Go backends on Apple Silicon (M1/M2/M3), Intel Macs, and Windows.
## Prerequisites ## Prerequisites
- **Node.js** (v18+) - **Node.js** (v18+)
- **Go** (v1.20+) - **Go** (v1.20+)
- **pnpm** (preferred) or npm - **pnpm** (preferred) or npm
- **C Compiler** (gcc/clang, via Xcode Command Line Tools on macOS)
## Development ## Development
### 1. Install Dependencies ### 1. Install Dependencies
```bash ```bash
npm install pnpm install
``` ```
### 2. Run in Development Mode ### 2. Run in Development Mode
@ -28,28 +30,25 @@ This starts the Go backend (port 36969) and the Vite/Electron frontend concurren
```bash ```bash
./start-go.sh ./start-go.sh
``` ```
*Note: Do not run `npm run dev` directly if you want the backend to work. Use the script.* *Note: Do not run `pnpm run dev` directly if you want the backend to work. Use the script.*
## Building for Production ## Building for Production
To create a distributable `.dmg` file for macOS: To create distributable release binaries (Universal `.dmg` for macOS, Portable `.exe` for Windows):
### 1. Build the App ### 1. Build the App
```bash ```bash
npm run build:mac # macOS Universal DMG
pnpm run build && pnpm run electron:build && npx electron-builder --mac --universal
# Windows Portable EXE
pnpm run build && pnpm run electron:build && npx electron-builder --win portable --x64
``` ```
This command will:
1. Compile the Go backend for both `amd64` and `arm64`.
2. Create a universal binary using `lipo`.
3. Build the React frontend.
4. Package the Electron app and bundle the backend.
5. Generate a universal `.dmg`.
### 2. Locate the Installer ### 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-1.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.

Binary file not shown.

Binary file not shown.

View file

@ -32,6 +32,8 @@ func GetScanTargets(category string) []string {
filepath.Join(home, "Library", "Logs"), filepath.Join(home, "Library", "Logs"),
filepath.Join(home, "Library", "Developer", "Xcode", "DerivedData"), filepath.Join(home, "Library", "Developer", "Xcode", "DerivedData"),
} }
case "trash":
return []string{filepath.Join(home, ".Trash")}
default: default:
return []string{} return []string{}
} }

View file

@ -1,16 +1,14 @@
package main package main
import ( import (
"embed"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"io/fs"
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"sort" "sort"
"strings"
"github.com/kv/clearnup/backend/internal/apps" "github.com/kv/clearnup/backend/internal/apps"
"github.com/kv/clearnup/backend/internal/cleaner" "github.com/kv/clearnup/backend/internal/cleaner"
@ -18,9 +16,6 @@ import (
"github.com/kv/clearnup/backend/internal/scanner" "github.com/kv/clearnup/backend/internal/scanner"
) )
//go:embed all:dist
var distFS embed.FS
const Port = ":36969" const Port = ":36969"
func enableCors(w *http.ResponseWriter) { func enableCors(w *http.ResponseWriter) {
@ -41,6 +36,8 @@ func main() {
http.HandleFunc("/api/empty-trash", handleEmptyTrash) http.HandleFunc("/api/empty-trash", handleEmptyTrash)
http.HandleFunc("/api/clear-cache", handleClearCache) http.HandleFunc("/api/clear-cache", handleClearCache)
http.HandleFunc("/api/clean-docker", handleCleanDocker) http.HandleFunc("/api/clean-docker", handleCleanDocker)
http.HandleFunc("/api/clean-xcode", handleCleanXcode)
http.HandleFunc("/api/clean-homebrew", handleCleanHomebrew)
http.HandleFunc("/api/system-info", handleSystemInfo) http.HandleFunc("/api/system-info", handleSystemInfo)
http.HandleFunc("/api/estimates", handleCleaningEstimates) http.HandleFunc("/api/estimates", handleCleaningEstimates)
@ -50,42 +47,8 @@ func main() {
http.HandleFunc("/api/apps/action", handleAppAction) http.HandleFunc("/api/apps/action", handleAppAction)
http.HandleFunc("/api/apps/uninstall", handleAppUninstall) http.HandleFunc("/api/apps/uninstall", handleAppUninstall)
// Static File Serving (SPA Support) // Static File Serving is handled directly by Electron.
// Check if we are running with embedded files or local dist // Backend only needs to provide API routes.
// Priority: Embedded (Production) -> Local dist (Dev/Preview)
// Try to get a sub-fs for "dist" from the embedded FS
distRoot, err := fs.Sub(distFS, "dist")
if err == nil {
fmt.Println("📂 Serving embedded static files")
// Check if it's actually populated (sanity check)
if _, err := distRoot.Open("index.html"); err == nil {
fsrv := http.FileServer(http.FS(distRoot))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if filepath.Ext(r.URL.Path) == "" {
// Read index.html from embedded
index, _ := distRoot.Open("index.html")
stat, _ := index.Stat()
http.ServeContent(w, r, "index.html", stat.ModTime(), index.(io.ReadSeeker))
return
}
fsrv.ServeHTTP(w, r)
})
} else {
// Fallback to local ./dist if embedded is empty (e.g. dev mode without build)
if _, err := os.Stat("dist"); err == nil {
fmt.Println("📂 Serving static files from local ./dist")
fs := http.FileServer(http.Dir("dist"))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if filepath.Ext(r.URL.Path) == "" {
http.ServeFile(w, r, "dist/index.html")
return
}
fs.ServeHTTP(w, r)
})
}
}
}
fmt.Printf("🚀 Antigravity Backend running on http://localhost%s\n", Port) fmt.Printf("🚀 Antigravity Backend running on http://localhost%s\n", Port)
@ -319,9 +282,18 @@ func handleCleanDocker(w http.ResponseWriter, r *http.Request) {
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
message := string(output)
if message == "" || len(message) > 500 { // fallback if output is empty mapping or huge
message = err.Error()
}
// If the daemon isn't running, provide a helpful message
if strings.Contains(message, "connect: no such file or directory") || strings.Contains(message, "Is the docker daemon running") {
message = "Docker daemon is not running. Please start Docker to clean it."
}
json.NewEncoder(w).Encode(map[string]interface{}{ json.NewEncoder(w).Encode(map[string]interface{}{
"cleared": 0, "cleared": 0,
"message": fmt.Sprintf("Docker cleanup failed: %s", err), "message": message,
}) })
return return
} }
@ -332,6 +304,61 @@ func handleCleanDocker(w http.ResponseWriter, r *http.Request) {
}) })
} }
func handleCleanXcode(w http.ResponseWriter, r *http.Request) {
enableCors(&w)
if r.Method == "OPTIONS" {
return
}
home, err := os.UserHomeDir()
if err != nil {
json.NewEncoder(w).Encode(map[string]interface{}{"cleared": 0, "message": "Could not find home directory"})
return
}
paths := []string{
filepath.Join(home, "Library/Developer/Xcode/DerivedData"),
filepath.Join(home, "Library/Developer/Xcode/iOS DeviceSupport"),
filepath.Join(home, "Library/Developer/Xcode/Archives"),
filepath.Join(home, "Library/Caches/com.apple.dt.Xcode"),
}
totalCleared := int64(0)
for _, p := range paths {
if stat, err := os.Stat(p); err == nil && stat.IsDir() {
size := scanner.GetDirectorySize(p)
if err := os.RemoveAll(p); err == nil {
totalCleared += size
}
}
}
json.NewEncoder(w).Encode(map[string]interface{}{"cleared": totalCleared, "message": "Xcode Caches Cleared"})
}
func handleCleanHomebrew(w http.ResponseWriter, r *http.Request) {
enableCors(&w)
if r.Method == "OPTIONS" {
return
}
cmd := exec.Command("brew", "cleanup", "--prune=all")
output, err := cmd.CombinedOutput()
if err != nil {
json.NewEncoder(w).Encode(map[string]interface{}{
"cleared": 0,
"message": fmt.Sprintf("Brew cleanup failed: %s", string(output)),
})
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"cleared": 1,
"message": "Homebrew Cache Cleared",
})
}
func handleSystemInfo(w http.ResponseWriter, r *http.Request) { func handleSystemInfo(w http.ResponseWriter, r *http.Request) {
enableCors(&w) enableCors(&w)
if r.Method == "OPTIONS" { if r.Method == "OPTIONS" {

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

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

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

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

View 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...');

View 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();
});

View 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();
});

View file

@ -1,5 +1,5 @@
{ {
"name": "Lumina", "name": "lumina",
"private": true, "private": true,
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",
@ -10,7 +10,9 @@
"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:go:win": "GOOS=windows GOARCH=amd64 go build -ldflags=\"-s -w\" -o backend/dist/windows/backend.exe backend/main.go",
"build:mac": "pnpm run build:go:mac && pnpm run build && pnpm run electron:build && electron-builder --mac --universal", "build:mac": "pnpm run build:go:mac && pnpm run build && pnpm run electron:build && electron-builder --mac --universal",
"build:win": "pnpm run build:go:win && pnpm run build && pnpm run electron:build && electron-builder --win portable --x64",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview", "preview": "vite preview",
"preinstall": "node scripts/check-pnpm.js" "preinstall": "node scripts/check-pnpm.js"
@ -64,18 +66,30 @@
], ],
"icon": "build/icon.png", "icon": "build/icon.png",
"category": "public.app-category.utilities", "category": "public.app-category.utilities",
"hardenedRuntime": false "hardenedRuntime": false,
},
"files": [
"dist/**/*",
"dist-electron/**/*",
"package.json"
],
"extraResources": [ "extraResources": [
{ {
"from": "backend/dist/universal/backend", "from": "backend/dist/universal/backend",
"to": "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"
]
} }
} }

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

View file

@ -147,6 +147,26 @@ export const API = {
} }
}, },
cleanXcode: async (): Promise<{ cleared: number; message: string }> => {
try {
const res = await fetch(`${API_BASE}/clean-xcode`, { method: "POST" });
return await res.json();
} catch (e) {
console.error(e);
return { cleared: 0, message: "Xcode cleanup failed" };
}
},
cleanHomebrew: async (): Promise<{ cleared: number; message: string }> => {
try {
const res = await fetch(`${API_BASE}/clean-homebrew`, { method: "POST" });
return await res.json();
} catch (e) {
console.error(e);
return { cleared: 0, message: "Homebrew cleanup failed" };
}
},
getSystemInfo: async (): Promise<SystemInfo | null> => { getSystemInfo: async (): Promise<SystemInfo | null> => {
try { try {
const res = await fetch(`${API_BASE}/system-info`); const res = await fetch(`${API_BASE}/system-info`);

View file

@ -44,11 +44,13 @@ export function Dashboard() {
const usage = await API.getDiskUsage(); const usage = await API.getDiskUsage();
if (usage) { if (usage) {
setDiskUsage(usage); setDiskUsage(usage);
// Update Tray Title with total free space from all drives // Update Tray Title with
const totalFree = usage.reduce((acc, drive) => acc + parseFloat(drive.freeGB), 0); if (usage && usage.length > 0) {
const mainDisk = usage[0];
if (window.electronAPI && window.electronAPI.updateTrayTitle) { if (window.electronAPI && window.electronAPI.updateTrayTitle) {
window.electronAPI.updateTrayTitle(`${totalFree.toFixed(2)} GB Free`) window.electronAPI.updateTrayTitle(`${mainDisk.freeGB} GB`)
.catch(e => console.error("Tray error", e)); .catch(e => console.error("Tray error:", e));
}
} }
} }
}; };
@ -273,7 +275,7 @@ export function Dashboard() {
onClick={async () => { onClick={async () => {
const confirmed = await toast.confirm( const confirmed = await toast.confirm(
'Start Flash Clean?', 'Start Flash Clean?',
'This will safely remove system caches, logs, trash, and Docker data.' 'This will safely remove system caches, logs, trash, Xcode caches, Homebrew cache, and Docker data.'
); );
if (confirmed) { if (confirmed) {
setIsScanning(true); setIsScanning(true);
@ -285,23 +287,35 @@ export function Dashboard() {
const cacheRes = await API.clearCache(); const cacheRes = await API.clearCache();
await API.emptyTrash(); await API.emptyTrash();
const dockerRes = await API.cleanDocker(); const dockerRes = await API.cleanDocker();
const xcodeRes = await API.cleanXcode();
const brewRes = await API.cleanHomebrew();
refreshDiskUsage(); refreshDiskUsage();
scanCategories(); scanCategories();
// Calculate total and formatted details // Calculate total and formatted details
const totalFreed = cacheRes.cleared + trashSize + (dockerRes.cleared || 0); const totalFreed = cacheRes.cleared + trashSize + (dockerRes.cleared || 0) + (xcodeRes.cleared || 0) + (brewRes.cleared || 0);
const details = []; const details = [];
if (cacheRes.cleared > 0) details.push('Cache'); if (cacheRes.cleared > 0) details.push('Cache');
if (trashSize > 0) details.push('Trash'); if (trashSize > 0) details.push('Trash');
if (dockerRes.cleared > 0) details.push('Docker'); if (dockerRes.cleared > 0) details.push('Docker');
if (xcodeRes.cleared > 0) details.push('Xcode');
if (brewRes.cleared > 0) details.push('Homebrew');
const detailStr = details.length > 0 ? ` (${details.join(', ')})` : ''; const detailStr = details.length > 0 ? ` (${details.join(', ')})` : '';
// Include the docker failure message if there was one
let finalMessage = `Freed ${formatBytes(totalFreed)}${detailStr}`;
if (dockerRes.message && dockerRes.cleared === 0 && details.length === 0 && totalFreed === 0) {
finalMessage = dockerRes.message;
} else if (dockerRes.message && dockerRes.cleared === 0) {
finalMessage += `\nNote: ${dockerRes.message}`;
}
toast.addToast({ toast.addToast({
type: 'success', type: dockerRes.message && totalFreed === 0 ? 'error' : 'success',
title: 'Flash Clean Complete', title: 'Flash Clean Complete',
message: `Freed ${formatBytes(totalFreed)}${detailStr} ` message: finalMessage
}); });
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -583,20 +597,8 @@ export function Dashboard() {
icon={<Icons.Trash />} icon={<Icons.Trash />}
label="Trash" label="Trash"
size={categorySizes.trash >= 0 ? formatBytes(categorySizes.trash) : "Not Scanned"} size={categorySizes.trash >= 0 ? formatBytes(categorySizes.trash) : "Not Scanned"}
onClick={async () => { onClick={() => runScan('trash', 'Trash Content')}
if (categorySizes.trash > 0) { actionIcon
const confirmed = await toast.confirm('Empty Trash?', 'This cannot be undone.');
if (confirmed) {
const success = await API.emptyTrash();
if (success) {
setCategorySizes(prev => ({ ...prev, trash: 0 }));
refreshDiskUsage();
toast.addToast({ type: 'success', title: 'Trash Emptied' });
}
}
}
}}
actionIcon={false}
highlight={categorySizes.trash > 0} highlight={categorySizes.trash > 0}
description="Deleted files in Trash" description="Deleted files in Trash"
/> />

View file

@ -26,7 +26,7 @@ GO_PID=$!
echo "✨ Starting Frontend..." echo "✨ Starting Frontend..."
# Check for pnpm or fallback # Check for pnpm or fallback
if command -v pnpm &> /dev/null; then if command -v pnpm &> /dev/null; then
pnpm run dev pnpm run dev:electron
else else
# Fallback if pnpm is also missing from PATH but bun is there # Fallback if pnpm is also missing from PATH but bun is there
bun run dev bun run dev