@@ -182,7 +182,7 @@ def main(
182182 log_file : Optional [str ],
183183 dry_run : bool ,
184184) -> None :
185- """Mac Deep Cleaner v2.0.1 — Professional macOS cleanup tool."""
185+ """Mac Deep Cleaner v2.2.0 — Professional macOS cleanup tool."""
186186 from core .dry_run import set_dry_run
187187 configure_logging (
188188 verbose = verbose ,
@@ -478,6 +478,10 @@ def render() -> Layout:
478478@click .option ("--notify" , is_flag = True , default = False )
479479@click .option ("--no-undo" , is_flag = True , default = False ,
480480 help = "Permanently delete instead of staging for undo." )
481+ @click .option ("--docker" , is_flag = True , default = False ,
482+ help = "Clean unused Docker containers, images, and networks." )
483+ @click .option ("--auto-with-cache" , is_flag = True , default = False ,
484+ help = "Prompt to delete system caches when using auto clean." )
481485@click .pass_context
482486def clean (
483487 ctx : click .Context ,
@@ -492,6 +496,8 @@ def clean(
492496 custom_roots : Tuple [str , ...],
493497 notify : bool ,
494498 no_undo : bool ,
499+ auto_with_cache : bool ,
500+ docker : bool ,
495501) -> None :
496502 """Interactively clean orphaned app leftovers and junk.
497503
@@ -513,6 +519,14 @@ def clean(
513519 if dry_run :
514520 console .print ("[yellow]Dry-run enabled; clean will run in preview mode.[/yellow]" )
515521
522+ clean_system_caches = False
523+ if auto_with_cache and not dry_run :
524+ from rich .prompt import Confirm
525+ clean_system_caches = Confirm .ask (
526+ "[bold red]Do you want to delete ~/Library/Caches as well? (System caches)[/bold red]" ,
527+ default = False
528+ )
529+
516530 _run (
517531 delete = not dry_run ,
518532 auto = auto ,
@@ -527,9 +541,77 @@ def clean(
527541 undo_mode = undo_mode ,
528542 dev_junk = dev_junk ,
529543 dev_junk_global = dev_junk_global ,
544+ clean_system_caches = clean_system_caches ,
545+ docker = docker ,
530546 )
531547
532548
549+ # ══════════════════════════════════════════════════════════════════════════════
550+ # ══════════════════════════════════════════════════════════════════════════════
551+ # UNINSTALL & DNS FLUSH
552+ # ══════════════════════════════════════════════════════════════════════════════
553+
554+ @main .command ()
555+ @click .argument ("app_name" )
556+ @click .option ("--auto" , is_flag = True , default = False , help = "Skip confirmation prompt" )
557+ @click .option ("--no-undo" , is_flag = True , default = False , help = "Permanently delete instead of staging for undo" )
558+ @click .pass_context
559+ def uninstall (ctx : click .Context , app_name : str , auto : bool , no_undo : bool ) -> None :
560+ """Deep uninstall an application and all its data."""
561+ from core .uninstaller import find_app_candidates , build_uninstall_plan , execute_uninstall
562+ from core .scanner import discover_installed_apps
563+ from core .undo import new_session
564+ from rich .prompt import Confirm
565+ from utils import bytes_human
566+
567+ apps = discover_installed_apps ()
568+ candidates = find_app_candidates (app_name , apps )
569+ if not candidates :
570+ console .print (f"[red]Could not find an installed app matching '{ app_name } '.[/red]" )
571+ return
572+
573+ app = candidates [0 ]
574+ if len (candidates ) > 1 :
575+ console .print (f"[yellow]Multiple apps matched. Selecting { app .name } ({ app .bundle_id } ).[/yellow]" )
576+
577+ plan = build_uninstall_plan (app )
578+
579+ console .print (f"\n [bold red]Uninstall Plan for { app .name } [/bold red]" )
580+ console .print (f"Total size: { bytes_human (plan .total_size )} " )
581+ for item in plan .deletable_items :
582+ console .print (f" [dim]- { item .path } [/dim] ([yellow]{ bytes_human (item .size )} [/yellow])" )
583+
584+ do_it = auto or Confirm .ask (f"\n Uninstall { app .name } and delete { bytes_human (plan .total_size )} ?" , default = False )
585+ if do_it :
586+ cfg = load_config ()
587+ session = new_session () if (cfg .undo_mode and not no_undo ) else None
588+ res = execute_uninstall (plan , session = session )
589+ console .print (f"\n [green]✓ Freed { bytes_human (res .bytes_freed )} . Removed { res .deleted } items. Staged { res .staged } .[/green]" )
590+
591+ @main .command ("flush-dns" )
592+ def flush_dns () -> None :
593+ """Flush DNS and network caches."""
594+ from core .dns_cache import flush_dns_cache
595+
596+ console .print ("Flushing DNS caches..." )
597+ res = flush_dns_cache ()
598+ if res .success :
599+ console .print ("[bold green]✓ DNS cache flushed successfully.[/bold green]" )
600+ else :
601+ console .print ("[bold red]✗ Failed to flush DNS cache.[/bold red]" )
602+
603+ @main .command ("tui" )
604+ def tui () -> None :
605+ """Launch the interactive Terminal User Interface."""
606+ try :
607+ import textual
608+ except ImportError :
609+ console .print ("[red]Textual is not installed. Run 'poetry install' or install textual manually.[/red]" )
610+ return
611+
612+ from tui .app import run_tui
613+ run_tui ()
614+
533615# ══════════════════════════════════════════════════════════════════════════════
534616# DEVELOPER JUNK
535617# ══════════════════════════════════════════════════════════════════════════════
@@ -3622,14 +3704,15 @@ def cmd_schedule() -> None:
36223704
36233705@cmd_schedule .command ("install" )
36243706@click .option ("--no-notify" , is_flag = True , default = False )
3707+ @click .option ("--clean" , is_flag = True , default = False , help = "Run background clean instead of just scan." )
36253708@click .pass_context
3626- def schedule_install (ctx : click .Context , no_notify : bool ) -> None :
3709+ def schedule_install (ctx : click .Context , no_notify : bool , clean : bool ) -> None :
36273710 """Install a weekly LaunchAgent to run scans automatically."""
36283711 from core .dry_run import skip_if_dry_run
36293712 if skip_if_dry_run (ctx , console , "schedule install" ):
36303713 return
36313714 from core .scheduler import install_schedule
3632- ok , msg = install_schedule (notify = not no_notify )
3715+ ok , msg = install_schedule (notify = not no_notify , clean = clean )
36333716 color = "green" if ok else "red"
36343717 console .print (f"\n [{ color } ]{ msg } [/{ color } ]\n " )
36353718
@@ -3779,6 +3862,8 @@ def _run(
37793862 dev_junk_global : bool = False ,
37803863 ci : bool = False ,
37813864 threshold_mb : int = 0 ,
3865+ clean_system_caches : bool = False ,
3866+ docker : bool = False ,
37823867) -> None :
37833868 """Core scan + optional cleanup logic (shared by scan and clean)."""
37843869 # Local import to satisfy static analysis and avoid name-resolution issues.
@@ -3984,10 +4069,31 @@ def _run_step(description: str, fn):
39844069 except Exception as exc :
39854070 logger .debug ("Failed to send notification: %s" , exc )
39864071
3987- if grand == 0 :
4072+ if grand == 0 and not docker :
39884073 console .print ("\n [bold green]✓ Your Mac is spotless! Nothing to clean.[/bold green]\n " )
39894074 return
39904075
4076+ if docker :
4077+ from scanners .docker_cleaner import scan_docker_bloat , clean_docker_bloat
4078+ bloat = scan_docker_bloat ()
4079+ if bloat :
4080+ console .print ()
4081+ console .rule ("[bold]Docker Junk" , style = "blue" )
4082+ docker_total = sum (e .size for e in bloat )
4083+ console .print (f" [blue]Docker Bloat:[/blue] { bytes_human (docker_total )} " )
4084+
4085+ do_del = auto
4086+ if not do_del :
4087+ from rich .prompt import Confirm
4088+ do_del = Confirm .ask (
4089+ f" Delete Docker bloat ([yellow]{ bytes_human (docker_total )} [/yellow])?" ,
4090+ default = False ,
4091+ )
4092+ if do_del :
4093+ freed_docker = clean_docker_bloat ()
4094+ console .print (f" [green]✓ Freed { bytes_human (freed_docker )} from Docker[/green]" )
4095+ grand += freed_docker
4096+
39914097 # Diff hint
39924098 try :
39934099 from config .history import list_history , diff_scans
@@ -4035,7 +4141,11 @@ def _run_step(description: str, fn):
40354141 if ok :
40364142 freed += sz
40374143
4038- user_junk = [j for j in junk if not j .is_system ]
4144+ if clean_system_caches :
4145+ user_junk = junk
4146+ else :
4147+ user_junk = [j for j in junk if not j .is_system ]
4148+
40394149 if user_junk :
40404150 do_del = auto
40414151 if not do_del :
@@ -4087,7 +4197,7 @@ def _run_step(description: str, fn):
40874197 f"[dim]Restore with: mac-cleaner undo --session { session .session_id [:8 ]} [/dim]"
40884198 )
40894199 else :
4090- freed = do_cleanup (orphans , junk , auto = auto )
4200+ freed = do_cleanup (orphans , junk , auto = auto , clean_system_caches = clean_system_caches )
40914201 if dev_junk_entries :
40924202 from rich .prompt import Confirm
40934203 do_del = auto or Confirm .ask (
0 commit comments