Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .github/workflows/desktop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: Desktop App

on:
push:
branches: [main, master]
paths:
- 'desktop/**'
- 'src/**'
- '.github/workflows/desktop.yml'
pull_request:
branches: [main, master]
paths:
- 'desktop/**'
- 'src/**'
- '.github/workflows/desktop.yml'
release:
types: [published]
workflow_dispatch:

jobs:
build:
runs-on: macos-latest

permissions:
contents: write

steps:
- name: Checkout
uses: actions/checkout@v7

- name: Setup Bun
uses: oven-sh/setup-bun@v2

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22

- name: Install CLI dependencies
run: bun install

- name: Install desktop dependencies
working-directory: desktop
run: bun install

- name: Type check
working-directory: desktop
run: bun run typecheck

- name: Test
working-directory: desktop
run: bun run test

- name: Build app (.dmg / .zip)
working-directory: desktop
run: bun run dist
env:
# Optional: set these repository secrets to ship a signed and notarized app.
CSC_LINK: ${{ secrets.MAC_CERTIFICATE_P12_BASE64 }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}

- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: mac-cleaner-desktop
path: |
desktop/release/*.dmg
desktop/release/*.zip
if-no-files-found: error

- name: Attach to GitHub release
if: github.event_name == 'release'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload "${{ github.event.release.tag_name }}" desktop/release/*.dmg desktop/release/*.zip --clobber
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
## [Unreleased]

### Added
- **Desktop app (`desktop/`)** — a macOS menu bar app built with Electron on top of the CLI scanners: daily automatic clean of safe categories (runs on wake if the Mac was asleep), one-click Clean Now, Deep Clean with a reviewable preview and explicit confirmation before permanently deleting, run history, notifications, launch at login and Full Disk Access guidance. Packaged as `.dmg`/`.zip` for Apple Silicon and Intel by the new `Desktop App` workflow and attached to GitHub releases
- **`scan` command** — scan without deleting; supports `--category`, `--verbose`, and `--json` for scripts and integrations
- **`clean` command** — non-interactive cleaning for automation: `--all`, `--categories <ids>`, `--yes`, `--dry-run`, `--unsafe`
- **Visual size bars** in the category picker to spot the biggest space savings at a glance (idea from #46 by @alibosworth)
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ That's it. No installation needed. The CLI will:
3. ✅ **Let you select** exactly what to clean
4. 🗑️ **Clean** only what you chose

## 🖥️ Desktop App (menu bar) — new

Prefer not to open a terminal? **Mac Cleaner** is a menu bar app built on the same scanners as the CLI:

- 🧹 **Cleans automatically every day** (temp files, browser cache, Homebrew cache, logs — you choose)
- ⚡ **Clean Now** from the menu bar whenever you want
- 🔥 **Deep Clean** — scan more places, review what was found and permanently delete it
- 📊 Shows how much space every run freed, with a full history

Download the latest `.dmg` from [Releases](https://github.com/guhcostan/mac-cleaner-cli/releases). See [`desktop/`](desktop/README.md) for details and how to build it yourself.

## 🎬 See It In Action

<p align="center">
Expand Down Expand Up @@ -244,6 +255,16 @@ bun run lint # Run linter
bun run build # Build for production
```

Desktop app (Electron, lives in [`desktop/`](desktop/README.md)):

```bash
cd desktop
bun install
bun run start # Build and launch the menu bar app
bun run test # Run desktop tests
bun run dist # Package .dmg/.zip into desktop/release (macOS only)
```

## 🤝 Contributing

Contributions are very welcome! Whether it's a new scanner, a bug fix, or a documentation improvement — we'd love your help.
Expand Down
3 changes: 3 additions & 0 deletions desktop/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
dist/
release/
108 changes: 108 additions & 0 deletions desktop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Mac Cleaner — Desktop (menu bar app)

A macOS menu bar app that keeps your Mac clean on its own. It uses the same scanners as
[`mac-cleaner-cli`](../README.md) (they are bundled straight from `../src`), so every
category, safety rule and protected-path check of the CLI applies here too.

<p align="center"><img src="build/icon.png" width="96" alt="Mac Cleaner icon"></p>

## What it does

| | |
|---|---|
| **Lives in the menu bar** | A sparkle icon at the top of the screen. Click it to open the popover; right click for a quick menu. There is no Dock icon. |
| **Daily automatic clean** | Once a day at the time you pick (default 10:00) it quietly cleans the categories you selected. If the Mac was asleep or off at that time, it runs as soon as it wakes. It never runs twice on the same day, and right after install it waits for the first scheduled time instead of cleaning immediately. |
| **Clean Now** | Runs the same selection as the daily clean, on demand. |
| **Deep Clean (hard delete)** | Scans many more places (system caches, dev caches, node_modules, Docker, Trash… and, if you opt in, risky ones like old Downloads or iOS backups), shows what was found per category with the largest items, and permanently deletes only what you tick, after an explicit confirmation. |
| **Shows what it removed** | Every run is saved to History with the space freed per category and any errors. The home screen shows the total freed and the last run. A notification is shown after each automatic clean. |

### Safety

- Unattended runs (daily and Clean Now) can only use categories that are **not** marked risky. This is enforced in the main process, not just in the UI.
- Automatic runs skip temp files touched in the last 24 hours (configurable) because running apps may still be using them.
- Deep Clean only deletes exactly what a scan preview showed and you confirmed. If the preview is older than 15 minutes, it refuses and asks you to scan again instead of rescanning on its own.
- All deletions go through the CLI's `removeItem`, which refuses system paths, `/` and your home folder, and never follows symlinks.
- The UI runs in a sandboxed renderer with a strict Content Security Policy and no Node.js access; it can only call the handful of actions exposed by the preload script.

### Default automatic categories

`temp-files`, `browser-cache`, `homebrew`, `system-logs`. You can add any other non-risky category (User Cache, Trash, Development Cache, Docker…) in **Settings**.

## Install

Download `Mac-Cleaner-<version>-arm64.dmg` (Apple Silicon) or `-x64.dmg` (Intel) from
[Releases](https://github.com/guhcostan/mac-cleaner-cli/releases), open it and drag
**Mac Cleaner** to Applications.

Then grant **Full Disk Access** (System Settings → Privacy & Security → Full Disk Access →
add Mac Cleaner). Without it macOS hides some caches from the app; the popover shows a
banner with a shortcut when access is missing.

> **Unsigned builds:** until the project has an Apple Developer ID, release builds are
> ad-hoc signed. macOS will say the app "cannot be opened because the developer cannot be
> verified". Right click the app → **Open** → **Open** once, or run
> `xattr -dr com.apple.quarantine "/Applications/Mac Cleaner.app"`.

## Develop

Requires Bun (or npm) and Node 22+. The app bundles code from the CLI, so install the root
dependencies too.

```bash
bun install # at the repository root
cd desktop
bun install
bun run start # build (with sourcemaps) and launch Electron
bun run test # unit tests for the scheduler, settings, store and clean engine
bun run typecheck
bun run dist # package .dmg + .zip for arm64 and x64 into desktop/release (macOS only)
```

State (settings + history) is stored in `~/Library/Application Support/Mac Cleaner/state.json`.

### Layout

```
desktop/
├── src/main/ Electron main process
│ ├── main.ts tray icon, popover window, IPC, scheduler timer, notifications
│ ├── controller.ts orchestrates scans/cleans, busy guard, state for the UI (Electron-free)
│ ├── engine.ts scan + clean on top of the CLI scanners (Electron-free)
│ ├── schedule.ts "is the daily run due?" logic (pure)
│ ├── settings.ts defaults + validation of settings coming from the UI
│ └── store.ts JSON persistence with atomic writes
├── src/preload/ the only bridge between UI and main process (window.cleaner)
├── src/renderer/ popover UI (plain TypeScript + CSS, no framework)
├── src/shared/ types shared by main, preload and renderer
├── assets/ menu bar template icons
├── build/ app icon + entitlements for packaging
└── scripts/ esbuild bundler and icon generator
```

### Why Electron

The scanners are TypeScript, so Electron lets the app reuse them as-is (no rewrite in
Swift, no bundled Node runtime to call the CLI through a subprocess), keeps one language in
the repo, and lets the logic be unit-tested on any OS. The trade-off is a bigger download
(~100 MB) than a native SwiftUI `MenuBarExtra` app would have.

## Releasing

The **Desktop App** workflow (`.github/workflows/desktop.yml`) builds the app on macOS for
every PR/push that touches `desktop/` or `src/`, uploads the `.dmg`/`.zip` as workflow
artifacts, and attaches them to the GitHub release when a release is published.

To ship a signed and notarized app, add these repository secrets:

| Secret | Value |
|---|---|
| `MAC_CERTIFICATE_P12_BASE64` | base64 of your "Developer ID Application" certificate (.p12) |
| `MAC_CERTIFICATE_PASSWORD` | password of the .p12 |
| `APPLE_ID` | Apple ID email used for notarization |
| `APPLE_APP_SPECIFIC_PASSWORD` | app-specific password for that Apple ID |
| `APPLE_TEAM_ID` | your 10-character team ID |

Without them the build is ad-hoc signed (see the note in [Install](#install)).

Bump `version` in `desktop/package.json` before a release: it is the version shown in the
app and in the file names.
Binary file added desktop/assets/trayTemplate.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added desktop/assets/trayTemplate@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added desktop/branding/logo-mark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added desktop/branding/menubar-black.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added desktop/branding/menubar-white.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions desktop/build/entitlements.mac.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?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>
<!-- Required by Electron's V8 JIT under the hardened runtime -->
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>
Binary file added desktop/build/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading