feat(cli): uffs --uninstall — guided, complete UFFS removal (M0-M11)#495
Merged
Conversation
Wire the --uninstall management command and its argument surface, per docs/dev/architecture/UFFS-Uninstall-Implementation-Plan.md (M0: tasks U-01, U-03). - dispatch.rs: add Command::Uninstall, the token map, the command-suggest list, the dispatch arm, and a from_token test. - commands/uninstall/args.rs: UninstallArgs + UninstallScope parser (--dry-run / --yes / --keep-config / --no-deep-sweep / --no-path / --scope / --json / --help). Pure, no IO, 9 unit tests. - commands/uninstall/mod.rs: run_uninstall entry + --help text; a temporary M0 scaffold notice until the analysis/removal phases land. Build + strict clippy clean; all uffs-cli tests pass. Analysis, plan, consent, and removal phases (M1+) follow as further commits on this branch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Read-only analysis: reuse the self-update Phase-A detection and show every discovered UFFS binary in OS search order, with the copy a bare command runs flagged ACTIVE (the rest shadowed / off-path). Tasks U-02, U-10, U-12 (partial). - update/mod.rs: widen detect() / model / binaries / procinfo to pub(crate) so uninstall reuses the one scanner (behavior-preserving; update tests stay green). - uninstall/resolve_order.rs: pure resolution ordering (Candidate -> ResolvedBinary, case-insensitive PATH rank, ACTIVE / shadowed / off-path), group_and_resolve by stem; 6 unit tests. - uninstall/analyze.rs: flatten the DetectionReport into candidates + build the OS executable search-dir list (process dir, system dirs, cwd, PATH). - uninstall/render.rs: print the resolution table. - uninstall/mod.rs: wire `uffs --uninstall` to run the analysis. Artifact inventory (U-11) and --json (U-12) follow on this branch. Build + strict clippy clean; all uffs-cli tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Complete the read-only analysis (tasks U-11, U-12). - uninstall/inventory.rs: resolve every non-binary trace — the data (%LOCALAPPDATA%\uffs), cache (secure_cache_dir), legacy cache (%TEMP%\uffs_index_cache), and per-user config dirs — each with existence + recursive size, plus broker-service state via uffs_winsvc::is_installed. Dedupes by path (config == data on macOS) so removal never double-counts. - uninstall/render.rs: print the inventory (integer-math human byte sizes, no float casts) + the broker service line; full --json (pure analysis_json, unit-tested) of binaries + artifacts + broker state. - uninstall/mod.rs: collect the inventory; --json emits JSON and returns early. `uffs --uninstall` and `--json` now show the complete removal surface, read-only. Build + strict clippy clean; all uffs-cli tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… (M2, M3) Build the ordered removal plan from the analysis and gate it on elevation — still entirely read-only (no removal engine yet). Tasks U-20, U-21, U-30. - uninstall/plan.rs: pure build_plan(report, inventory, args) -> RemovalPlan. Groups in safe order (Services -> Processes -> Binaries -> Data/cache/config); WinGet roots become a `winget uninstall` delegation, never a hand-delete; per-item needs_elevation + coarse scope; honors --keep-config / --scope. 6 unit tests (winget-delegated, machine-needs-elevation, service-first, keep-config, scope-user-excludes-service, process-stop). - uninstall/render.rs: print_plan (numbered consent surface + reclaimed bytes), print_elevation_refusal, and the plan in --json (pure plan_json). - uninstall/mod.rs: build the plan; --dry-run prints it and stops; the M3 elevation gate refuses before any effect when the plan needs Administrator the current process lacks (uffs_winsvc::is_elevated). `uffs --uninstall --dry-run` now prints the full ordered plan; --json carries it too. Build + strict clippy clean; all uffs-cli tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the uninstall plan's `Action` enum + string `description` with a
structured `PlanTarget` enum that holds the execution data (process pid,
service name, root dir + binary stems, winget package id + scope, dir to
delete). This is the single source of truth that both the renderer
(description / --json) and the upcoming removal executor (M4b) consume, so what
is shown is exactly what gets removed.
- plan.rs: PlanTarget { StopProcess | RemoveService | DeleteBinaries |
DelegateWinget | DeleteDir } with action_label() + describe(); PlanItem now
holds `target` instead of action + description; RemovalPlan::items() exposed.
Tests match on target variants.
- render.rs: derive the plan description + json action from item.target.
Output is equivalent (the winget item now also shows its scope). Build + strict
clippy clean; all uffs-cli tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On Mac/Linux the uninstall now runs at user level unless a binary root is actually unwritable by the user. A root-owned /usr/local/bin install is flagged before the executor tries; ~/bin, ~/.cargo/bin, and dev builds need no sudo. Answers "do we really need sudo on posix?": no, unless ownership says so. - uffs-mft platform::system: dir_user_writable(dir) — POSIX access(W_OK) probe (unix-only), exported via uffs_mft::platform. Lives beside is_elevated / geteuid, keeping the libc/unsafe in the platform crate (uffs-cli stays unsafe-free). - uninstall/plan.rs: binaries_need_escalation(scope, dir) — Windows: machine scope; Unix: !dir_user_writable. cfg-gated fns (idiomatic, no unused-param noise). binary_item derives needs_elevation from it. +2 cfg-specific tests. - uninstall/mod.rs: the elevation gate now uses cross-platform uffs_mft::platform::is_elevated (Windows token / Unix euid == 0) instead of the Windows-only uffs_winsvc stub (which is false off Windows). - uninstall/render.rs: refusal names sudo (posix) / elevated shell (windows). Verified: dev root under $HOME → access(W_OK) ok → no escalation. Build + strict clippy clean (uffs-mft + uffs-cli); all tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implement the destructive engine behind the consent + elevation gates. - uninstall/remove.rs: Effects trait + execute() — walks the ordered plan, dispatches each structured target to the injected sink, and records a per-item outcome. Best-effort: a failing item is recorded and the rest still run (one locked file never strands the cleanup). RecordingEffects fake + 2 executor tests (group order; failure recorded while others continue) — zero real deletions in tests. - uninstall/effects.rs: live SystemEffects — std::fs deletes (idempotent via a try_exists "confirmed absent" check), process stop (kill/taskkill), service removal (uffs_winsvc::stop + sc delete), winget uninstall delegation. Shells out rather than via libc, so the CLI stays unsafe-free. - uninstall/render.rs: print_outcome (counts + failures + retry hint). - uninstall/mod.rs: after the elevation gate, prompt for consent (default No; --yes skips), then execute and report. Empty plan / declined → clean no-op. `uffs --uninstall --dry-run` stays read-only; the real path runs only on explicit confirmation. Build + strict clippy clean; all uffs-cli tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Offer to drop PATH entries that point at a removed UFFS root — only an exact (case-insensitive) match to an unmanaged/dev root we deleted, so it is provably UFFS and safe. WinGet roots and mixed entries are never touched; --no-path skips the group entirely. - plan.rs: PlanTarget::RemovePathEntry; build_plan takes the live PATH and adds a PATH group for matching removed roots (machine-scope PATH flagged elevated). +1 test (offered on match, suppressed by --no-path, untouched on no match). - analyze.rs: path_entries() — the split PATH. - remove.rs: Effects::remove_path_entry + dispatch + recording fake. - effects.rs: Windows edits the persisted user + machine PATH via PowerShell (each scope guarded so a write — and thus elevation — happens only when that scope has the entry; SetEnvironmentVariable broadcasts WM_SETTINGCHANGE). Unix writes a manual-cleanup hint (the shell owns PATH). Build + strict clippy clean; all uffs-cli tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Eat our own dog food: while the daemon is up, ask UFFS itself to find stray family files (uffs*.exe, *_compact.uffs, *_usn.cursor, ...) anywhere on the indexed drives, beyond the known install roots. Strays are REPORTED for review, never auto-removed — a uffs.exe under Downloads may be the user's own copy. - sweep.rs: Search trait + find_strays (pure: per-pattern search, drop hits already under a planned dir, sort + dedup; separator-aware prefix so /opt/uffs never matches /opt/uffs-other). DaemonSearch live backend via uffs_client search_cli_raw, best-effort (no daemon → no hits, never fails) with defensive recursive JSON path extraction. 3 tests (dedup/filter, sibling-prefix, json extraction). - render.rs: print_strays (review-only listing). - mod.rs: run the sweep after the plan (skipped by --no-deep-sweep); plan_dirs helper feeds the known-dir filter. Build + strict clippy clean; all uffs-cli tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- effects.rs: schedule_self_delete — the running uffs.exe (+ uffs-update.exe) cannot delete their own image on Windows, so spawn a detached cmd that waits for this process to exit then deletes them (the classic self-delete, no FFI). Unix unlinks them directly. - verify.rs: still_present — re-stat the targeted locations after removal and report any that survived (daemon-free; the search service is gone by then). 2 tests. - render.rs: print_verification (clean / leftovers) + print_self_delete_warning. - mod.rs: after execute, schedule the self-delete (warn honestly if even that fails), then verify the remaining locations (excluding the reboot-deferred self-binaries). Build + strict clippy clean; all uffs-cli tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Right-sized resume. The uninstall is idempotent (deletes are try_exists-guarded, service/winget removals no-op when gone, the self-delete is reboot-deferred), so "resume" is just re-running it — re-detection finds and removes whatever is left. That is the key difference from the non-idempotent self-update swaps that genuinely need a full replay journal. So M9 is a small in-progress marker written to the system temp dir (which survives the lifecycle-dir deletion): - journal.rs: begin / finish / was_interrupted over a temp-dir marker; idempotent clear; round-trip test. - mod.rs: on launch, note an interrupted prior run; mark in-progress before removal; clear on a clean finish — each warning surfaced honestly, never blocking the uninstall. - render.rs: print_resumed_note + print_journal_warning. Build + strict clippy clean; all uffs-cli tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…test (M11) - docs/user-manual/uninstall.md: full user guide (steps, elevation table, WinGet delegation, flags, what-gets-removed, safety). Linked from the manual index next to Updating. - CHANGELOG: ## [Unreleased] entry for `uffs --uninstall`. - effects.rs: U-112 — a real test of the live SystemEffects delete path on throwaway temp files (idempotent delete_binaries + remove_dir); no UFFS install is touched. Completes the uninstall implementation plan (M0-M11). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The cfg(windows) uninstall code is only linted by the cross (xwin) clippy in the
pre-push gate, which flagged four nits macOS clippy never sees:
- effects.rs schedule_self_delete: build the del-command string via map→Vec→join
instead of format!-collect (format_collect).
- effects.rs remove_windows_service: discard the best-effort `uffs_winsvc::stop`
result with an explicit `match { Ok(()) | Err(_) => {} }` (no non-binding
`let _` on a must-use).
- plan.rs binaries_need_escalation (Windows variant): make it `const fn`.
macOS + `cargo xwin clippy --target x86_64-pc-windows-msvc -D warnings` both
clean; all uffs-cli tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A single command,
uffs --uninstall, that removes UFFS and all of its data from the machine, as carefully asuffs --update. Implements the full plan (M0–M11), one milestone per commit.It analyzes the install (every binary shown in OS-resolution order,
ACTIVEcopy flagged), inventories every artifact with sizes, runs a deep sweep (UFFS searching for itself), prints an itemized ordered plan, and only removes after explicit consent (or--yes), then verifies the result.Highlights
sudo— a realaccess(W_OK)writability check (added touffs_mft::platform, keeping the CLIunsafe-free) decides per root; only root-owned locations / the Windows broker service / machine installs need elevation.winget uninstall, never hand-deleted.--dry-runreviews without changing anything; removal is best-effort (a locked item is reported, the rest proceed) and idempotent (re-run to finish an interrupted one). Conservative PATH edits (only entries pointing exactly at a removed root); strays listed, never auto-deleted; running binary self-deletes on exit; post-removal verify.Design
--json+ human) and execution.Effectstrait → orchestration unit-tested with a recording fake (zero real deletions in tests), plus a live-path test on throwaway temp files.Quality
uffs-clitests; strict clippy clean (incl.cargo xwin clippy --target x86_64-pc-windows-msvc -D warningsfor thecfg(windows)paths); fulllint-pre-pushgate green.docs/user-manual/uninstall.md(linked from the manual index) + CHANGELOG Unreleased entry.🤖 Generated with Claude Code