rebecca 0.2.0

Windows-first cleanup CLI and Rust library surface for Rebecca cleanup planning, rules, and platform adapters.
Documentation
use anyhow::Result;
use clap::{CommandFactory, Parser};
use clap_complete::{Shell, generate};
use std::io;

mod apps;
mod cache;
mod cache_view;
mod clean;
mod clean_view;
mod cli;
mod history_view;
mod info;
mod output;
mod purge;
mod purge_view;
mod scan;

use cli::{
    AppsCommand, CacheCommand, CleanArgs, Cli, Command, CompletionArgs, ConfigCommand,
    DoctorCommand, HistoryArgs, OutputMode, PurgeArgs, ScanArgs,
};

fn main() {
    init_tracing();

    if let Err(err) = run() {
        if err.downcast_ref::<output::MachineErrorRendered>().is_some() {
            std::process::exit(1);
        }

        let cli = Cli::try_parse();
        let (command, mode) = cli
            .as_ref()
            .ok()
            .map(|cli| (command_name(&cli.command), command_output_mode(cli)))
            .unwrap_or(("rebecca", OutputMode::Human));
        output::render_error(command, mode, &err);
        std::process::exit(1);
    }
}

fn run() -> Result<()> {
    let cli = if std::env::args_os().len() <= 1 {
        let mut cmd = Cli::command();
        cmd.print_help()?;
        println!();
        return Ok(());
    } else {
        Cli::parse()
    };

    match cli.command {
        Command::Scan(args) => run_scan(args, cli.format),
        Command::Clean(args) => run_clean(args, cli.format),
        Command::Purge(args) => run_purge(args, cli.format),
        Command::History(args) => run_history(args, cli.format),
        Command::Cache { command } => match command {
            CacheCommand::Purge { dry_run, yes } => cache::purge(cache::CachePurgeOptions {
                dry_run,
                output_mode: cli.format,
                yes,
            }),
        },
        Command::Apps { command } => match command {
            AppsCommand::Scan {
                no_progress,
                scan_cache,
                exclude_paths,
            } => apps::scan(apps::AppsScanOptions {
                output_mode: cli.format,
                no_progress,
                scan_cache,
                exclude_paths,
            }),
            AppsCommand::Clean {
                dry_run,
                yes,
                no_progress,
                scan_cache,
                exclude_paths,
            } => apps::clean(apps::AppsCleanOptions {
                dry_run,
                output_mode: cli.format,
                yes,
                no_progress,
                scan_cache,
                exclude_paths,
            }),
        },
        Command::Config { command } => match command {
            ConfigCommand::Paths => info::print_config_paths(cli.format),
        },
        Command::Doctor { command } => match command {
            DoctorCommand::Permissions => info::print_privilege_level(cli.format),
        },
        Command::Completion(args) => run_completion(args),
    }
}

fn run_scan(args: ScanArgs, global_mode: OutputMode) -> Result<()> {
    scan::run(global_mode, args.categories, args.rules)
}

fn run_clean(args: CleanArgs, global_mode: OutputMode) -> Result<()> {
    let CleanArgs {
        dry_run,
        yes,
        selection,
        execution,
        risk,
    } = args;
    clean::run(clean::CleanOptions {
        dry_run,
        output_mode: global_mode,
        yes,
        no_progress: execution.no_progress,
        scan_cache: execution.scan_cache,
        categories: selection.categories,
        rules: selection.rules,
        exclude_paths: execution.exclude_paths,
        allow_moderate: risk.allow_moderate,
        allow_risky: risk.allow_risky,
    })
}

fn run_purge(args: PurgeArgs, global_mode: OutputMode) -> Result<()> {
    purge::run(purge::PurgeOptions {
        dry_run: args.dry_run,
        output_mode: global_mode,
        yes: args.yes,
        no_progress: args.no_progress,
        scan_cache: args.scan_cache,
        list_artifacts: args.list_artifacts,
        roots: args.roots,
        max_depth: args.max_depth,
        min_age_days: args.min_age_days,
        artifacts: args.artifacts,
        exclude_paths: args.exclude_paths,
    })
}

fn run_history(args: HistoryArgs, global_mode: OutputMode) -> Result<()> {
    info::print_history(global_mode, args.limit)
}

fn run_completion(args: CompletionArgs) -> Result<()> {
    let shell = args.shell.unwrap_or_else(default_completion_shell);
    let mut cmd = Cli::command();
    let bin_name = cmd.get_name().to_owned();
    generate(shell, &mut cmd, bin_name, &mut io::stdout());
    Ok(())
}

fn default_completion_shell() -> Shell {
    Shell::from_env().unwrap_or(Shell::Bash)
}

fn init_tracing() {
    let _ = tracing_subscriber::fmt()
        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
        .try_init();
}

fn command_output_mode(cli: &Cli) -> OutputMode {
    cli.format
}

fn command_name(command: &Command) -> &'static str {
    match command {
        Command::Scan(_) => "scan",
        Command::Clean(_) => "clean",
        Command::Purge(_) => "purge",
        Command::History(_) => "history",
        Command::Cache { command } => match command {
            CacheCommand::Purge { .. } => "cache purge",
        },
        Command::Apps { command } => match command {
            AppsCommand::Scan { .. } => "apps scan",
            AppsCommand::Clean { .. } => "apps clean",
        },
        Command::Config { command } => match command {
            ConfigCommand::Paths => "config paths",
        },
        Command::Doctor { command } => match command {
            DoctorCommand::Permissions => "doctor permissions",
        },
        Command::Completion(_) => "completion",
    }
}