cargo-reclaim 0.2.1

Safe Cargo cleanup for target directories, stale artifacts, and Cargo home caches
Documentation
use std::io::Write;
use std::path::{Path, PathBuf};

use cargo_reclaim::{Plan, PlanEntry, PolicyKind};

use super::labels::{action_label, artifact_label, policy_label, skip_reason_label};
use crate::cli::{CliError, PlanMode};

pub(super) fn write_help(output: &mut impl Write) -> Result<(), CliError> {
    writeln!(output, "cargo-reclaim")?;
    writeln!(
        output,
        "Usage: cargo-reclaim [OPTIONS] [ROOT ...]\n       cargo-reclaim list [OPTIONS] [ROOT ...]\n       cargo-reclaim <scan|plan|apply|edit-plan|scheduler|cargo-config|cargo-home> [OPTIONS]"
    )?;
    writeln!(output)?;
    writeln!(output, "Commands:")?;
    writeln!(
        output,
        "  [ROOT ...]  Open the TTY cleanup assistant; use --all for bulk smart trim and --target with --delete-target for explicit whole-target deletion"
    )?;
    writeln!(output, "  scan    Build a read-only dry-run plan for roots")?;
    writeln!(output, "  plan    Build a read-only dry-run plan for roots")?;
    writeln!(output, "  apply   Validate or execute a persisted plan")?;
    writeln!(
        output,
        "  edit-plan  List, edit, or interactively select entries in a persisted plan"
    )?;
    writeln!(output, "  list    Read-only target inventory with sizes")?;
    writeln!(
        output,
        "  scheduler preview  Preview scheduler artifacts only"
    )?;
    writeln!(
        output,
        "  scheduler install --dry-run  Plan scheduler artifact installation"
    )?;
    writeln!(
        output,
        "  scheduler uninstall --dry-run  Plan scheduler artifact removal"
    )?;
    writeln!(
        output,
        "  cargo-home report  Report Cargo home caches and preserved paths"
    )?;
    writeln!(
        output,
        "  cargo-home plan  Build a dry-run Cargo home cleanup plan"
    )?;
    writeln!(
        output,
        "  cargo-home apply --plan <path> [--yes]  Validate or execute a persisted Cargo home cleanup plan"
    )?;
    writeln!(
        output,
        "  cargo-config recommend  Report read-only Cargo build output configuration recommendations"
    )?;
    writeln!(
        output,
        "  cargo-config preview  Preview read-only Cargo config write plan"
    )?;
    writeln!(
        output,
        "  cargo-config apply --preview <path> --yes  Apply a Cargo config preview"
    )?;
    writeln!(output)?;
    writeln!(output, "Options:")?;
    writeln!(
        output,
        "  --config <path>              Load scan/plan defaults from a TOML config file"
    )?;
    writeln!(
        output,
        "  --policy <kind>              observe, conservative, balanced, aggressive, custom"
    )?;
    writeln!(
        output,
        "  --whole-target <mode>        off, confirm, delete; delete requires aggressive policy"
    )?;
    writeln!(
        output,
        "  --ignore <path>              Report a path as ignored while scanning"
    )?;
    writeln!(
        output,
        "  --skip <path>                Do not scan a path or its descendants"
    )?;
    writeln!(
        output,
        "  --allow-name-only-targets    Include weak target/ matches as confirmation-only"
    )?;
    writeln!(
        output,
        "  --follow-symlinks            Follow symlinks during scan and inventory"
    )?;
    writeln!(
        output,
        "  --keep-days <days>           Preserve recently modified delete candidates for N days"
    )?;
    writeln!(
        output,
        "  --keep-size <size>           Preserve delete candidates at or below size"
    )?;
    writeln!(
        output,
        "  --keep-rustc-hash <u64>      Preserve fingerprint grouped intermediates for a rustc hash"
    )?;
    writeln!(
        output,
        "  --keep-installed-toolchains  Preserve fingerprint groups for installed rustup toolchains"
    )?;
    writeln!(
        output,
        "  --keep-toolchain <name>      Preserve fingerprint groups for a named rustup toolchain"
    )?;
    writeln!(
        output,
        "  --cross-filesystems          Allow recursive scan across filesystem boundaries"
    )?;
    writeln!(
        output,
        "  --keep-recent-writes <dur>   Preserve delete candidates modified within s, m, h, or d"
    )?;
    writeln!(
        output,
        "  --json                       Emit one dry-run JSON plan document"
    )?;
    Ok(())
}

pub(super) fn write_plan(
    output: &mut impl Write,
    plan: &Plan,
    policy: PolicyKind,
    mode: PlanMode,
) -> Result<(), CliError> {
    let mode_label = match mode {
        PlanMode::Scan => "scan",
        PlanMode::Plan => "plan",
    };

    writeln!(output, "cargo-reclaim {mode_label} dry-run")?;
    writeln!(output, "dry-run only; no files were deleted or modified")?;
    writeln!(
        output,
        "human-readable text; use --json for a stable structured document"
    )?;
    writeln!(output, "policy: {}", policy_label(policy))?;
    writeln!(output, "roots: {}", join_paths(&plan.input.roots))?;
    writeln!(output, "entries: {}", plan.totals.entry_count)?;
    writeln!(
        output,
        "delete candidates: {}",
        plan.totals.delete_candidate_count
    )?;
    writeln!(output, "preserved/unknown: {}", plan.totals.preserved_count)?;
    writeln!(output, "estimated bytes: {}", plan.totals.total_bytes)?;
    writeln!(
        output,
        "skipped scan paths: {}",
        plan.totals.skipped_path_count
    )?;

    if !plan.skipped_paths.is_empty() {
        writeln!(output)?;
        writeln!(output, "skipped scan path details:")?;
        for skip in &plan.skipped_paths {
            match &skip.message {
                Some(message) => writeln!(
                    output,
                    "  {}\t{}\t{}",
                    skip_reason_label(skip.reason),
                    display_path(&skip.path),
                    display_text(message)
                )?,
                None => writeln!(
                    output,
                    "  {}\t{}",
                    skip_reason_label(skip.reason),
                    display_path(&skip.path)
                )?,
            }
        }
    }

    if plan.entries.is_empty() {
        writeln!(
            output,
            "no reclaimable or reportable target artifacts found"
        )?;
        return Ok(());
    }

    writeln!(output)?;
    for entry in &plan.entries {
        write_entry(output, entry)?;
    }

    Ok(())
}

fn write_entry(output: &mut impl Write, entry: &PlanEntry) -> Result<(), CliError> {
    writeln!(
        output,
        "{}\t{}\t{}\t{}\t{}",
        action_label(&entry.action),
        artifact_label(entry.artifact_class),
        entry.snapshot.size_bytes,
        display_path(&entry.snapshot.path),
        display_text(&entry.policy_reason)
    )?;
    Ok(())
}

fn join_paths(paths: &[PathBuf]) -> String {
    paths
        .iter()
        .map(|path| display_path(path))
        .collect::<Vec<_>>()
        .join(", ")
}

fn display_path(path: &Path) -> String {
    display_text(&path.display().to_string())
}

fn display_text(value: &str) -> String {
    value.escape_default().to_string()
}