Compare commits

...

No commits in common. "5f5643cf42ea7f9b327aa1617072c71c86358d14" and "eb4a4007365cefa42a8af731bbebc302e9678d69" have entirely different histories.

35 changed files with 9886 additions and 9263 deletions

3
.gitignore vendored
View file

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

View file

@ -48,7 +48,7 @@ This command will:
### 2. Locate the Installer
The output file will be at:
```
release/KV Clearnup-0.0.0-universal.dmg
release/KV Clearnup-1.0.0-universal.dmg
```
## Running the App

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", "Developer", "Xcode", "DerivedData"),
}
case "trash":
return []string{filepath.Join(home, ".Trash")}
default:
return []string{}
}

View file

@ -11,6 +11,7 @@ import (
"os/exec"
"path/filepath"
"sort"
"strings"
"github.com/kv/clearnup/backend/internal/apps"
"github.com/kv/clearnup/backend/internal/cleaner"
@ -41,6 +42,8 @@ func main() {
http.HandleFunc("/api/empty-trash", handleEmptyTrash)
http.HandleFunc("/api/clear-cache", handleClearCache)
http.HandleFunc("/api/clean-docker", handleCleanDocker)
http.HandleFunc("/api/clean-xcode", handleCleanXcode)
http.HandleFunc("/api/clean-homebrew", handleCleanHomebrew)
http.HandleFunc("/api/system-info", handleSystemInfo)
http.HandleFunc("/api/estimates", handleCleaningEstimates)
@ -319,9 +322,18 @@ func handleCleanDocker(w http.ResponseWriter, r *http.Request) {
output, err := cmd.CombinedOutput()
if err != nil {
message := string(output)
if message == "" || len(message) > 500 { // fallback if output is empty mapping or huge
message = err.Error()
}
// If the daemon isn't running, provide a helpful message
if strings.Contains(message, "connect: no such file or directory") || strings.Contains(message, "Is the docker daemon running") {
message = "Docker daemon is not running. Please start Docker to clean it."
}
json.NewEncoder(w).Encode(map[string]interface{}{
"cleared": 0,
"message": fmt.Sprintf("Docker cleanup failed: %s", err),
"message": message,
})
return
}
@ -332,6 +344,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) {
enableCors(&w)
if r.Method == "OPTIONS" {

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

@ -0,0 +1,467 @@
"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 backendPath = import_path3.default.join(process.resourcesPath, "backend");
console.log("Starting backend from:", backendPath);
try {
backendProcess = (0, import_child_process3.spawn)(backendPath, [], {
stdio: "inherit"
});
backendProcess.on("error", (err) => {
console.error("Failed to start backend:", err);
});
backendProcess.on("exit", (code, signal) => {
console.log(`Backend exited with code ${code} and signal ${signal}`);
});
} catch (error) {
console.error("Error spawning backend:", error);
}
};
function createTray() {
const iconPath = import_path3.default.join(__dirname, "../dist/tray/tray-iconTemplate.png");
let finalIconPath = iconPath;
if (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

@ -49,12 +49,14 @@ function createTray() {
// Check if dist/tray exists, if not try public/tray (dev mode)
let finalIconPath = iconPath;
if (!fs.existsSync(iconPath)) {
if (process.env.NODE_ENV === 'development') {
finalIconPath = path.join(__dirname, '../public/tray/tray-iconTemplate.png');
}
const image = nativeImage.createFromPath(finalIconPath);
tray = new Tray(image.resize({ width: 16, height: 16 }));
let image = nativeImage.createFromPath(finalIconPath);
image.setTemplateImage(true);
tray = new Tray(image.resize({ width: 18, height: 18 }));
tray.setToolTip('Antigravity Cleaner');
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,7 +1,7 @@
{
"name": "Lumina",
"private": true,
"version": "0.0.0",
"version": "1.0.0",
"type": "module",
"main": "dist-electron/main.cjs",
"scripts": {
@ -10,7 +10,7 @@
"electron:build": "node scripts/build-electron.mjs",
"build": "tsc -b && vite build",
"build:go:mac": "sh scripts/build-go.sh",
"build:mac": "npm run build:go:mac && npm run build && npm run electron:build && electron-builder --mac --universal",
"build:mac": "pnpm run build:go:mac && pnpm run build && pnpm run electron:build && electron-builder --mac --universal",
"lint": "eslint .",
"preview": "vite preview",
"preinstall": "node scripts/check-pnpm.js"
@ -40,7 +40,7 @@
"globals": "^16.5.0",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.17",
"typescript": "~5.9.3",
"typescript": "^5.3.3",
"typescript-eslint": "^8.46.4",
"vite": "^7.2.4",
"wait-on": "^8.0.1"
@ -66,6 +66,12 @@
"category": "public.app-category.utilities",
"hardenedRuntime": false
},
"win": {
"target": [
"portable"
],
"icon": "build/icon.png"
},
"files": [
"dist/**/*",
"dist-electron/**/*",

View file

@ -76,7 +76,7 @@ importers:
specifier: ^3.4.17
version: 3.4.19
typescript:
specifier: ~5.9.3
specifier: ^5.3.3
version: 5.9.3
typescript-eslint:
specifier: ^8.46.4
@ -97,20 +97,20 @@ packages:
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
'@babel/code-frame@7.28.6':
resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==}
'@babel/code-frame@7.29.0':
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
engines: {node: '>=6.9.0'}
'@babel/compat-data@7.28.6':
resolution: {integrity: sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==}
'@babel/compat-data@7.29.0':
resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==}
engines: {node: '>=6.9.0'}
'@babel/core@7.28.6':
resolution: {integrity: sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==}
'@babel/core@7.29.0':
resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==}
engines: {node: '>=6.9.0'}
'@babel/generator@7.28.6':
resolution: {integrity: sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==}
'@babel/generator@7.29.0':
resolution: {integrity: sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==}
engines: {node: '>=6.9.0'}
'@babel/helper-compilation-targets@7.28.6':
@ -151,8 +151,8 @@ packages:
resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==}
engines: {node: '>=6.9.0'}
'@babel/parser@7.28.6':
resolution: {integrity: sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==}
'@babel/parser@7.29.0':
resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==}
engines: {node: '>=6.0.0'}
hasBin: true
@ -172,12 +172,12 @@ packages:
resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
engines: {node: '>=6.9.0'}
'@babel/traverse@7.28.6':
resolution: {integrity: sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==}
'@babel/traverse@7.29.0':
resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==}
engines: {node: '>=6.9.0'}
'@babel/types@7.28.6':
resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==}
'@babel/types@7.29.0':
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
engines: {node: '>=6.9.0'}
'@develar/schema-utils@2.6.5':
@ -2524,25 +2524,25 @@ snapshots:
'@alloc/quick-lru@5.2.0': {}
'@babel/code-frame@7.28.6':
'@babel/code-frame@7.29.0':
dependencies:
'@babel/helper-validator-identifier': 7.28.5
js-tokens: 4.0.0
picocolors: 1.1.1
'@babel/compat-data@7.28.6': {}
'@babel/compat-data@7.29.0': {}
'@babel/core@7.28.6':
'@babel/core@7.29.0':
dependencies:
'@babel/code-frame': 7.28.6
'@babel/generator': 7.28.6
'@babel/code-frame': 7.29.0
'@babel/generator': 7.29.0
'@babel/helper-compilation-targets': 7.28.6
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.28.6)
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
'@babel/helpers': 7.28.6
'@babel/parser': 7.28.6
'@babel/parser': 7.29.0
'@babel/template': 7.28.6
'@babel/traverse': 7.28.6
'@babel/types': 7.28.6
'@babel/traverse': 7.29.0
'@babel/types': 7.29.0
'@jridgewell/remapping': 2.3.5
convert-source-map: 2.0.0
debug: 4.4.3
@ -2552,17 +2552,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@babel/generator@7.28.6':
'@babel/generator@7.29.0':
dependencies:
'@babel/parser': 7.28.6
'@babel/types': 7.28.6
'@babel/parser': 7.29.0
'@babel/types': 7.29.0
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
jsesc: 3.1.0
'@babel/helper-compilation-targets@7.28.6':
dependencies:
'@babel/compat-data': 7.28.6
'@babel/compat-data': 7.29.0
'@babel/helper-validator-option': 7.27.1
browserslist: 4.28.1
lru-cache: 5.1.1
@ -2572,17 +2572,17 @@ snapshots:
'@babel/helper-module-imports@7.28.6':
dependencies:
'@babel/traverse': 7.28.6
'@babel/types': 7.28.6
'@babel/traverse': 7.29.0
'@babel/types': 7.29.0
transitivePeerDependencies:
- supports-color
'@babel/helper-module-transforms@7.28.6(@babel/core@7.28.6)':
'@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.28.6
'@babel/core': 7.29.0
'@babel/helper-module-imports': 7.28.6
'@babel/helper-validator-identifier': 7.28.5
'@babel/traverse': 7.28.6
'@babel/traverse': 7.29.0
transitivePeerDependencies:
- supports-color
@ -2597,41 +2597,41 @@ snapshots:
'@babel/helpers@7.28.6':
dependencies:
'@babel/template': 7.28.6
'@babel/types': 7.28.6
'@babel/types': 7.29.0
'@babel/parser@7.28.6':
'@babel/parser@7.29.0':
dependencies:
'@babel/types': 7.28.6
'@babel/types': 7.29.0
'@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.6)':
'@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.28.6
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.6)':
'@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.28.6
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
'@babel/template@7.28.6':
dependencies:
'@babel/code-frame': 7.28.6
'@babel/parser': 7.28.6
'@babel/types': 7.28.6
'@babel/code-frame': 7.29.0
'@babel/parser': 7.29.0
'@babel/types': 7.29.0
'@babel/traverse@7.28.6':
'@babel/traverse@7.29.0':
dependencies:
'@babel/code-frame': 7.28.6
'@babel/generator': 7.28.6
'@babel/code-frame': 7.29.0
'@babel/generator': 7.29.0
'@babel/helper-globals': 7.28.0
'@babel/parser': 7.28.6
'@babel/parser': 7.29.0
'@babel/template': 7.28.6
'@babel/types': 7.28.6
'@babel/types': 7.29.0
debug: 4.4.3
transitivePeerDependencies:
- supports-color
'@babel/types@7.28.6':
'@babel/types@7.29.0':
dependencies:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
@ -3059,24 +3059,24 @@ snapshots:
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.28.6
'@babel/types': 7.28.6
'@babel/parser': 7.29.0
'@babel/types': 7.29.0
'@types/babel__generator': 7.27.0
'@types/babel__template': 7.4.4
'@types/babel__traverse': 7.28.0
'@types/babel__generator@7.27.0':
dependencies:
'@babel/types': 7.28.6
'@babel/types': 7.29.0
'@types/babel__template@7.4.4':
dependencies:
'@babel/parser': 7.28.6
'@babel/types': 7.28.6
'@babel/parser': 7.29.0
'@babel/types': 7.29.0
'@types/babel__traverse@7.28.0':
dependencies:
'@babel/types': 7.28.6
'@babel/types': 7.29.0
'@types/cacheable-request@6.0.3':
dependencies:
@ -3232,9 +3232,9 @@ snapshots:
'@vitejs/plugin-react@5.1.2(vite@7.3.1(@types/node@24.10.9)(jiti@1.21.7))':
dependencies:
'@babel/core': 7.28.6
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.6)
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.6)
'@babel/core': 7.29.0
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0)
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0)
'@rolldown/pluginutils': 1.0.0-beta.53
'@types/babel__core': 7.20.5
react-refresh: 0.18.0
@ -3802,8 +3802,8 @@ snapshots:
eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@1.21.7)):
dependencies:
'@babel/core': 7.28.6
'@babel/parser': 7.28.6
'@babel/core': 7.29.0
'@babel/parser': 7.29.0
eslint: 9.39.2(jiti@1.21.7)
hermes-parser: 0.25.1
zod: 4.3.6

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> => {
try {
const res = await fetch(`${API_BASE}/system-info`);

View file

@ -44,11 +44,13 @@ export function Dashboard() {
const usage = await API.getDiskUsage();
if (usage) {
setDiskUsage(usage);
// Update Tray Title with total free space from all drives
const totalFree = usage.reduce((acc, drive) => acc + parseFloat(drive.freeGB), 0);
// Update Tray Title with
if (usage && usage.length > 0) {
const mainDisk = usage[0];
if (window.electronAPI && window.electronAPI.updateTrayTitle) {
window.electronAPI.updateTrayTitle(`${totalFree.toFixed(2)} GB Free`)
.catch(e => console.error("Tray error", e));
window.electronAPI.updateTrayTitle(`${mainDisk.freeGB} GB`)
.catch(e => console.error("Tray error:", e));
}
}
}
};
@ -273,7 +275,7 @@ export function Dashboard() {
onClick={async () => {
const confirmed = await toast.confirm(
'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) {
setIsScanning(true);
@ -285,23 +287,35 @@ export function Dashboard() {
const cacheRes = await API.clearCache();
await API.emptyTrash();
const dockerRes = await API.cleanDocker();
const xcodeRes = await API.cleanXcode();
const brewRes = await API.cleanHomebrew();
refreshDiskUsage();
scanCategories();
// 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 = [];
if (cacheRes.cleared > 0) details.push('Cache');
if (trashSize > 0) details.push('Trash');
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(', ')})` : '';
// 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({
type: 'success',
type: dockerRes.message && totalFreed === 0 ? 'error' : 'success',
title: 'Flash Clean Complete',
message: `Freed ${formatBytes(totalFreed)}${detailStr} `
message: finalMessage
});
} catch (e) {
console.error(e);
@ -583,20 +597,8 @@ export function Dashboard() {
icon={<Icons.Trash />}
label="Trash"
size={categorySizes.trash >= 0 ? formatBytes(categorySizes.trash) : "Not Scanned"}
onClick={async () => {
if (categorySizes.trash > 0) {
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}
onClick={() => runScan('trash', 'Trash Content')}
actionIcon
highlight={categorySizes.trash > 0}
description="Deleted files in Trash"
/>

View file

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

BIN
tracked_files.txt Normal file

Binary file not shown.