Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
dd38e9b
feat(status): implement streaming watch mode and extended metrics (Ph…
baroitrung Oct 14, 2026
ccc0649
feat(mgmt): implement biometrics, self-update, safe removal, and inte…
baroitrung Oct 14, 2026
69f30f5
feat(optimize): implement Phase 3 optimize expansion and opt-in clean…
baroitrung Oct 14, 2026
89f12c5
feat(parity): implement mu history, mu purge, and mu installer (Phase 4)
baroitrung Oct 14, 2026
6dcf8ae
feat(clean-uninstall): implement Phase 2 core clean and uninstall par…
baroitrung Oct 14, 2026
f5de702
fix(clean-uninstall): resolve MU-003 Phase 2 review findings 1-7
baroitrung Oct 16, 2026
230f5ab
fix(clean): review r1 findings 1,5,6,7
baroitrung Oct 16, 2026
5ef37f7
fix(uninstall): review r1 findings 2,3,4
baroitrung Oct 16, 2026
a4b78ad
fix(clean): refuse symlinked trash subdirs on external volumes (r2 f1)
baroitrung Oct 16, 2026
c775f9b
merge(optimize): integrate quaqua/mu-p3-integ (Phase 3) into P2 branch
baroitrung Oct 16, 2026
6a0f1db
fix(clean,optimize): resolve Phase 3 review findings 1-6
baroitrung Oct 16, 2026
d8fcbd8
fix(clean,optimize): address mufix-p3 review r1 findings
baroitrung Oct 16, 2026
bbb14d3
merge(parity): integrate quaqua/mu-p4-integ (Phase 4) into P3 branch
baroitrung Oct 19, 2026
7cad4aa
fix(purge,installer): resolve Phase 4 review findings 1-4
baroitrung Oct 19, 2026
1ecc023
merge(parity): integrate quaqua/mu-p5-integ (Phase 5) into mu-integ b…
baroitrung Oct 19, 2026
a6ae282
fix(status,cli): resolve Phase 5 review findings 1-5 (process ranking…
baroitrung Oct 19, 2026
213c5f7
fix(cli): reject sub-nanosecond zero durations and fix rustfmt chaining
baroitrung Oct 19, 2026
fa97776
merge(parity): integrate quaqua/mu-p6-integ (Phase 6) into mu-integ b…
baroitrung Oct 19, 2026
b2d42af
fix(mgmt): resolve Phase 6 review findings 1-10 (whitelist, sudo pam,…
baroitrung Oct 19, 2026
122313e
fix(p6): resolve findings 1-8 and 9 (clippy, tests, xdg data, malform…
baroitrung Oct 19, 2026
987249d
fix(optimize): resolve clippy type_complexity and collapsible_if warn…
baroitrung Oct 19, 2026
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ ratatui = "0.30"
crossterm = "0.29"
nix = { version = "0.31", features = ["fs", "process", "user"] }
time = { version = "0.3", features = ["local-offset", "formatting"] }
sha2 = "0.10"

[profile.release]
# Go built with `-s -w` — match by stripping symbols from the release binary.
Expand Down
22 changes: 11 additions & 11 deletions docs/plans/active/mole-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ updated: 2026-10-14
- `make test` passes with zero regressions across all unit and integration suites (312+ baseline tests preserved).
- Interactive Disk Analysis (Phase 1): `mu analyze` provides an interactive ncdu-like Ratatui disk usage explorer with `--json`, `--dry-run` immutability proof, and in-app safe trash deletion.
- Clean & Uninstall enhancements (Phase 2): Snap/Flatpak browser caches detected, `system-trash` clean target reclaims space safely with size and item count preview via `gio trash --empty` (failing closed without `--yes` in headless mode), preview ledger exported to `$XDG_CONFIG_HOME/mu/clean-list.txt`, `mu uninstall <pkg>` executes non-interactively from CLI, `mu uninstall --list` outputs installed packages, and `--permanent` is explicitly rejected with a helpful safety error.
- Optimize & Clean extensions (Phase 3): `mu optimize` expands from 3 to 9 safe maintenance units (DNS cache flush, broken package config repair, failed systemd unit reset, unused Flatpak runtime cleanup, SQLite vacuum, stale state cleanup, broken autostart audit, broken services audit, and home permissions audit) with `--skip` support and exact shell command display in `--dry-run`; optional clean targets for developer CLI caches (`--include=dev-cache`), Podman (`--include=podman`), and orphaned system services (`--include=orphaned-services`). The dangerous route/neighbor table flush is dropped and marked N/A as it severs live SSH connections.
- Optimize & Clean extensions (Phase 3): `mu optimize` expands from 3 to 9 safe maintenance units (DNS cache flush, broken package config repair, failed systemd unit reset, unused Flatpak runtime cleanup, SQLite vacuum, stale state cleanup, broken autostart audit, broken services audit, and home permissions audit) with `--skip` support and exact shell command display in `--dry-run`; optional clean targets for developer CLI caches (`--include=dev-cache`), Podman (`--include=podman`), and orphaned user services (`--include=orphaned-services`). The dangerous route/neighbor table flush is dropped and marked N/A as it severs live SSH connections.
- History, Purge & Installer (Phase 4): `mu history` inspects `$XDG_DATA_HOME/mu/operations.log` in table and `--json` format; `mu purge` safely cleans heavy project build artifacts (`node_modules`, `target`, `dist`, `.venv`) with `--paths`, `--dry-run`, and `--yes`; `mu installer` finds and cleans large package installers in `~/Downloads`.
- Advanced Status Monitoring (Phase 5): `mu status` supports continuous NDJSON streaming via `--watch [--interval 1s]` and surfaces per-core CPU, disk I/O, battery, and top process stats.
- Management & Biometrics (Phase 6): `mu sudo` provides PAM fingerprint configuration (`enable`, `disable`, `status`) mirroring `mo touchid`; `mu update` checks GitHub releases and self-updates verified binaries; `mu remove` safely uninstalls `mu`; `mu completion --install` auto-installs shell scripts.
Expand Down Expand Up @@ -103,11 +103,11 @@ The following table catalogs every user-facing feature from upstream Mole (`tw93
| **Uninstall: Sensitive Data Review** | `lib/uninstall/review.sh:1-50` | **missing**: `src/uninstall/remove.rs` deletes all remnants together | In interactive TUI, provide a sub-item selection screen allowing user to uncheck user data/config directories while purging package binaries. |
| **Uninstall: Steam Game Detection** | `lib/uninstall/steam.sh` | **missing**: no Steam handling in `src/uninstall/` | Detect Steam app manifests under `~/.local/share/Steam/steamapps/appmanifest_*.acf` and provide uninstallation delegation via `steam steam://uninstall/<id>`. |
| **Optimize: Whitelist TUI Editor** | `bin/optimize.sh`, `lib/manage/whitelist.sh:32` | **missing**: `src/config.rs:49` reads `optimize_skip` from TOML | Provide interactive TUI screen to toggle `optimize_skip` steps in `~/.config/mu/config.toml`. |
| **Optimize: Flush DNS Cache** | `lib/optimize/catalog.sh:29, 37` | **missing**: not in `src/optimize.rs:92-121` | Implement step `dns`: execute `resolvectl flush-caches` (or fallback `systemd-resolve --flush-caches`). |
| **Optimize: Flush DNS Cache** | `lib/optimize/catalog.sh:29, 37` | **missing**: not in `src/optimize.rs:92-121` | Implement step `dns`: execute `sudo resolvectl flush-caches` (or fallback `sudo systemd-resolve --flush-caches`); `flush-caches` is polkit-gated, so the system step runs with sudo (Conflict 2). |
| **Optimize: Network Stack Refresh** | `lib/optimize/catalog.sh:45` | **N/A on Linux**: unsafe on Ubuntu | **DROPPED:** Route cache is obsolete on Linux >= 3.6; neighbor table flush (`ip neigh flush all`) drops active SSH/network sessions. Moved to N/A section. |
| **Optimize: Package Repair** | `lib/optimize/catalog.sh:35` | **missing**: not in `src/optimize.rs` | Implement step `packages`: execute `sudo dpkg --configure -a` and `sudo apt-get install -f -y`. |
| **Optimize: SQLite Database VACUUM** | `lib/optimize/catalog.sh:39` | **missing**: not in `src/optimize.rs` | Implement step `sqlite`: scan Firefox/Chrome/Thunderbird profiles and run `sqlite3 <db> "VACUUM;"` when browser processes are not running. |
| **Optimize: Systemd Failed Units** | Linux equivalent of launchd repair | **missing**: not in `src/optimize.rs` | Implement step `systemd`: execute `systemctl reset-failed` and `systemctl --user reset-failed`. |
| **Optimize: Systemd Failed Units** | Linux equivalent of launchd repair | **missing**: not in `src/optimize.rs` | Implement step `systemd`: execute `sudo systemctl reset-failed` (system scope is polkit-gated) and `systemctl --user reset-failed` (Conflict 2). |
| **Optimize: Flatpak Unused Runtimes** | Linux package orphan cleanup | **missing**: not in `src/optimize.rs` | Implement step `flatpak`: execute `flatpak uninstall --unused -y` if flatpak binary is present. |
| **Optimize: Stale App Saved State** | `lib/optimize/catalog.sh:34` | **missing**: not in `src/optimize.rs` | Implement step `state`: clean files older than 30 days in `~/.local/state/` matching known transient state patterns. |
| **Optimize: User Perms Repair** | `lib/optimize/catalog.sh:47` | **missing**: not in `src/optimize.rs` | Implement step `permissions`: audit files in `$HOME` owned by root or with broken permissions. |
Expand Down Expand Up @@ -273,9 +273,9 @@ graph TD
- `apt`: `sudo apt-get update && sudo apt-get autoremove --purge -y`
- `journal`: `sudo journalctl --vacuum-size=500M`
- `caches`: `sudo update-mime-database /usr/share/mime && fc-cache -f`
- `dns`: Flush DNS cache via `resolvectl flush-caches` (fallback `systemd-resolve --flush-caches`).
- `dns`: Flush DNS cache via `sudo resolvectl flush-caches` (fallback `sudo systemd-resolve --flush-caches`).
- `packages`: Repair interrupted package installs via `sudo dpkg --configure -a && sudo apt-get install -f -y`.
- `systemd`: Reset failed systemd units via `systemctl reset-failed && systemctl --user reset-failed`.
- `systemd`: Reset failed systemd units via `sudo systemctl reset-failed && systemctl --user reset-failed`.
- `flatpak`: Clean unused Flatpak runtimes via `flatpak uninstall --unused -y` (skipped if Flatpak absent).
- `sqlite`: Vacuum closed application databases (Firefox, Chrome, Thunderbird sqlite databases).
- `state`: Clean stale files older than 30 days in `~/.local/state/`.
Expand All @@ -287,9 +287,9 @@ graph TD
- `apt`: `[dry-run] sudo apt-get update && sudo apt-get autoremove --purge -y`
- `journal`: `[dry-run] sudo journalctl --vacuum-size=500M`
- `caches`: `[dry-run] sudo update-mime-database /usr/share/mime && fc-cache -f`
- `dns`: `[dry-run] resolvectl flush-caches`
- `dns`: `[dry-run] sudo resolvectl flush-caches`
- `packages`: `[dry-run] sudo dpkg --configure -a && sudo apt-get install -f -y`
- `systemd`: `[dry-run] systemctl reset-failed && systemctl --user reset-failed`
- `systemd`: `[dry-run] sudo systemctl reset-failed && systemctl --user reset-failed`
- `flatpak`: `[dry-run] flatpak uninstall --unused -y`
- `sqlite`: `[dry-run] sqlite3 <db_path> "VACUUM;"`
- `state`: `[dry-run] find ~/.local/state -type f -mtime +30 -delete`
Expand All @@ -298,7 +298,7 @@ graph TD
- `dev-cache` (`--include=dev-cache`): clean `~/.cache/pip`, `~/.npm/_cacache`, `~/.cargo/registry/cache`, `~/.cache/go-build`.
- `podman` (`--include=podman`): run `podman system prune -f` when Podman socket or binary is detected.
- `orphaned-app-data` (`--include=orphaned-app-data`): scan `$XDG_DATA_HOME` and `$XDG_CONFIG_HOME` for uninstalled packages.
- `orphaned-services` (`--include=orphaned-services`): clean broken systemd user and system unit symlinks.
- `orphaned-services` (`--include=orphaned-services`): clean broken systemd **user** unit symlinks, recursing `*.wants/` enablement dirs; system-scope units are audit-only (`optimize services`) since they cannot go through SafeDelete.
- **Files Likely Touched:**
- `src/optimize.rs`
- `src/clean/targets.rs`
Expand All @@ -312,7 +312,7 @@ graph TD
- Network route/neighbor table flush is excluded.
- **Concrete Acceptance Tests:**
1. `cargo test optimize::` -> `test result: ok; 0 failed`
2. `cargo run --release -- optimize --dry-run` -> exit 0; output lists registered steps showing the exact shell command string for each step (e.g. `[dry-run] sudo journalctl --vacuum-size=500M`, `[dry-run] resolvectl flush-caches`).
2. `cargo run --release -- optimize --dry-run` -> exit 0; output lists registered steps showing the exact shell command string for each step (e.g. `[dry-run] sudo journalctl --vacuum-size=500M`, `[dry-run] sudo resolvectl flush-caches`).
3. `cargo run --release -- optimize --dry-run --skip=apt,dns,packages` -> exit 0; output explicitly indicates `[skip]` for `apt`, `dns`, and `packages`.
4. Sentinel immutability test:
```bash
Expand Down Expand Up @@ -430,8 +430,8 @@ graph TD
- Interactive Ratatui screen to view and toggle entries in `~/.config/mu/config.toml`.
- **`mu update` (Self-Update):**
- Queries GitHub Releases API for `https://api.github.com/repos/.../releases/latest`.
- Downloads architecture-matched release binary and `checksums.txt`.
- Verifies SHA-256 checksum against signed asset.
- Downloads architecture-matched release binary (generic mu asset accepted only on x86_64) and checksums.txt.
- Verifies SHA-256 checksum against checksums.txt asset (fail-closed, matching install.sh).
- Atomically replaces current executable via `tempfile` and `rename`.
- Flags: `--dry-run`, `--force`.
- Background check displays update banner in root TUI menu.
Expand Down
3 changes: 3 additions & 0 deletions src/audit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,9 @@ pub(crate) mod test_support {
confirm: Arc::new(|_| panic!("confirm must not be reached")),
config_home: root.to_path_buf(),
data_home: root.join("data"),
home: root.to_path_buf(),
state_home: root.join("state"),
trash_deps: std::rc::Rc::new(crate::trash::TrashDeps::real()),
no_oplog: true,
},
}
Expand Down
59 changes: 54 additions & 5 deletions src/clean/flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ pub(crate) enum RunDispatch {
pub(crate) struct FlowModel {
opts: Options,
targets: Vec<CleanTarget>,
/// Display labels captured once at construction (`count_fn` may read the
/// filesystem) so the render path is free of filesystem reads.
display_labels: Vec<String>,
/// `state` — pub(crate) so the driver can cheaply test Scanning/Running.
pub(crate) state: FlowState,
/// Terminal width from `WindowSize`; default 80.
Expand Down Expand Up @@ -175,9 +178,13 @@ struct Scanned {
impl FlowModel {
/// `newFlowModel`.
pub(crate) fn new(opts: Options, targets: Vec<CleanTarget>) -> Self {
// Compute the display labels once, not on every render tick (finding 7
// r1: `display_label` may read the filesystem for the trash count).
let display_labels = targets.iter().map(CleanTarget::display_label).collect();
Self {
opts,
targets,
display_labels,
state: FlowState::Scanning,
width: 80,
spinner_idx: 0,
Expand Down Expand Up @@ -464,8 +471,8 @@ impl FlowModel {
let mut s = format!(" {} Scanning system", SPINNER_FRAMES[self.spinner_idx]);
let done = self.results.len();
if done > 0 {
let label = if done < self.targets.len() {
format!(": {}", self.targets[done].label)
let label = if done < self.display_labels.len() {
format!(": {}", self.display_labels[done])
} else {
String::new()
};
Expand All @@ -489,9 +496,9 @@ impl FlowModel {
size_w = size_w.max(size::human_size(res.size).chars().count());
}
let mut label_w = self
.targets
.display_labels
.iter()
.map(|t| t.label.chars().count())
.map(|l| l.chars().count())
.max()
.unwrap_or(0);
let max = self.width.saturating_sub(size_w + 6);
Expand All @@ -514,7 +521,7 @@ impl FlowModel {
),
];
for (i, res) in self.results.iter().enumerate() {
let label = truncate(self.targets[i].label, label_w as isize);
let label = truncate(&self.display_labels[i], label_w as isize);
lines.push(Line::raw(format!(
" {label}{}{:>size_w$}",
pad(&label),
Expand Down Expand Up @@ -802,6 +809,7 @@ mod tests {
Ok(())
}
}),
count_fn: None,
}
})
.collect()
Expand Down Expand Up @@ -1047,6 +1055,7 @@ mod tests {
)
})),
execute: Box::new(|_| Ok(())),
count_fn: None,
};
let m = FlowModel::new(Options::default(), vec![target]);
let Msg::ScanTarget(sm) = m.scan_cmd(0) else {
Expand Down Expand Up @@ -1143,6 +1152,7 @@ mod tests {
scan: Box::new(|| (0, None)),
preview: None,
execute: Box::new(|_| Ok(())),
count_fn: None,
}
}
fn plain_target() -> CleanTarget {
Expand All @@ -1154,6 +1164,7 @@ mod tests {
scan: Box::new(|| (0, None)),
preview: None,
execute: Box::new(|_| Ok(())),
count_fn: None,
}
}

Expand Down Expand Up @@ -1199,4 +1210,42 @@ mod tests {
};
assert!(done.err.is_none());
}

// Finding 6: the interactive summary must render the dynamic
// system-trash item count (`display_label`), not the static label.
#[test]
fn summary_shows_system_trash_item_count() {
let target = CleanTarget {
id: "system-trash",
label: "System Trash",
requires_sudo: false,
opt_in: true,
scan: Box::new(|| (SIZE_A, None)),
preview: None,
execute: Box::new(|_| Ok(())),
count_fn: Some(Box::new(|| "System Trash (2 items)".to_string())),
};
let mut m = FlowModel::new(Options::default(), vec![target]);
m.results = vec![Scanned {
size: SIZE_A,
items: Vec::new(),
}];
m.total = SIZE_A;
m.state = FlowState::Summary;
let text: String = m
.view_lines()
.iter()
.map(|l| {
l.spans
.iter()
.map(|s| s.content.as_ref())
.collect::<String>()
})
.collect::<Vec<_>>()
.join("\n");
assert!(
text.contains("System Trash (2 items)"),
"summary missing dynamic count label: {text:?}"
);
}
}
Loading
Loading