rebecca 0.3.0

Cross-platform cleanup CLI and Rust library surface for Rebecca cleanup planning, rules, and platform adapters.
Documentation
use anyhow::Result;
use rebecca::core::Platform;
use serde::Serialize;

use crate::cli::OutputMode;
use crate::output::CliApiContract;

#[derive(Debug, Serialize)]
struct CapabilitiesReport {
    api_version: &'static str,
    cli_version: &'static str,
    platform: PlatformReport,
    features: FeatureReport,
    output_formats: &'static [&'static str],
    schema_documents: &'static [&'static str],
    recommended_startup_commands: &'static [&'static str],
    commands: Vec<CommandCapability>,
    long_running_commands: &'static [&'static str],
    safety_model: SafetyModelReport,
}

#[derive(Debug, Serialize)]
struct PlatformReport {
    current: &'static str,
    cleanup_execution_supported: bool,
}

#[derive(Debug, Serialize)]
struct FeatureReport {
    rules: bool,
    windows: bool,
    ntfs: bool,
}

#[derive(Debug, Serialize)]
struct CommandCapability {
    name: &'static str,
    payload_kind: &'static str,
    machine_readable: bool,
    ndjson: bool,
    mutates_files: bool,
    availability: CommandAvailability,
    platforms: &'static [&'static str],
    schema_documents: &'static [&'static str],
    preflight_commands: &'static [&'static str],
    required_execution_flag: Option<&'static str>,
    required_confirmation_flags: &'static [&'static str],
    macos_privacy_relevant: bool,
}

#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "kebab-case")]
enum CommandAvailability {
    Available,
    PreviewOnly,
    UnsupportedPlatform,
}

#[derive(Debug, Serialize)]
struct SafetyModelReport {
    preview_default: bool,
    explicit_execution_flag: &'static str,
    recoverable_delete_default: bool,
    persistent_risky_opt_in: bool,
    external_rules_enabled_by_validation: bool,
}

const OUTPUT_FORMATS: &[&str] = &["human", "json", "ndjson"];
const SCHEMA_DOCUMENTS: &[&str] = &[
    "envelope",
    "event",
    "error",
    "payloads",
    "config",
    "cleaner-manifest-v1",
];
const ALL_PLATFORMS: &[&str] = &["windows", "linux", "macos"];
const CURRENT_PLATFORM_ONLY: &[&str] = &["current"];
const CLEANUP_SCHEMA_DOCS: &[&str] = &["payloads", "config", "cleaner-manifest-v1"];
const PAYLOAD_SCHEMA_DOCS: &[&str] = &["payloads"];
const CONFIG_SCHEMA_DOCS: &[&str] = &["payloads", "config"];
const RULE_SCHEMA_DOCS: &[&str] = &["payloads", "cleaner-manifest-v1"];
const CLEAN_PREFLIGHT: &[&str] = &[
    "doctor permissions",
    "doctor active-processes",
    "config show",
];
const MACOS_CLEAN_PREFLIGHT: &[&str] = &[
    "doctor permissions",
    "doctor active-processes",
    "catalog --kind cleanup-rule --platform macos",
    "config show",
];
const STARTUP_COMMANDS: &[&str] = &[
    "capabilities",
    "schema export --document payloads",
    "schema export --document config",
    "schema export --document cleaner-manifest-v1",
    "doctor permissions",
    "doctor active-processes",
    "catalog --kind cleanup-rule --platform current",
    "config show",
    "config paths",
];
const LONG_RUNNING_COMMANDS: &[&str] = &[
    "clean",
    "apps scan",
    "apps clean",
    "purge",
    "inspect artifacts",
    "inspect map",
    "inspect space",
];

pub fn run(output_mode: OutputMode) -> Result<()> {
    let report = capabilities_report();
    crate::output::print_command_success_with_contract(
        CliApiContract::v1("capabilities", "capabilities"),
        output_mode,
        || &report,
        || {
            print_human(&report);
            Ok(())
        },
    )
}

fn capabilities_report() -> CapabilitiesReport {
    CapabilitiesReport {
        api_version: "rebecca.cli.v1",
        cli_version: env!("CARGO_PKG_VERSION"),
        platform: PlatformReport {
            current: Platform::current().label(),
            cleanup_execution_supported: matches!(
                Platform::current(),
                Platform::Windows | Platform::Linux | Platform::Macos
            ),
        },
        features: FeatureReport {
            rules: cfg!(feature = "rules"),
            windows: cfg!(feature = "windows"),
            ntfs: cfg!(feature = "ntfs"),
        },
        output_formats: OUTPUT_FORMATS,
        schema_documents: SCHEMA_DOCUMENTS,
        recommended_startup_commands: STARTUP_COMMANDS,
        commands: command_capabilities(),
        long_running_commands: LONG_RUNNING_COMMANDS,
        safety_model: SafetyModelReport {
            preview_default: true,
            explicit_execution_flag: "--yes",
            recoverable_delete_default: true,
            persistent_risky_opt_in: false,
            external_rules_enabled_by_validation: false,
        },
    }
}

fn command_capabilities() -> Vec<CommandCapability> {
    vec![
        command("capabilities", "capabilities", false, false),
        command("schema export", "cli-schema", false, false)
            .with_schema_documents(SCHEMA_DOCUMENTS),
        command("catalog", "catalog", false, false),
        command("catalog validate", "catalog-validation", false, false),
        command("rules validate", "rule-validation", false, false)
            .with_schema_documents(RULE_SCHEMA_DOCS),
        command("rules import", "rule-import", true, false).with_schema_documents(RULE_SCHEMA_DOCS),
        command("rules list", "rule-import-list", false, false)
            .with_schema_documents(RULE_SCHEMA_DOCS),
        command("rules enable", "rule-import-mutation", true, false)
            .with_schema_documents(RULE_SCHEMA_DOCS),
        command("rules disable", "rule-import-mutation", true, false)
            .with_schema_documents(RULE_SCHEMA_DOCS),
        command("rules remove", "rule-import-mutation", true, false)
            .with_schema_documents(RULE_SCHEMA_DOCS),
        command("scan", "rule-catalog", false, false).with_schema_documents(CLEANUP_SCHEMA_DOCS),
        command("clean", "cleanup-plan", true, true)
            .with_schema_documents(CLEANUP_SCHEMA_DOCS)
            .with_preflight(clean_preflight_commands())
            .with_required_execution_flag("--yes")
            .with_required_confirmation_flags(&[
                "--allow-moderate",
                "--allow-risky",
                "--allow-warning",
            ])
            .with_macos_privacy_relevant(),
        command("apps scan", "app-leftovers-cleanup-plan", false, true)
            .with_schema_documents(PAYLOAD_SCHEMA_DOCS)
            .with_preflight(&["doctor active-processes"]),
        command("apps clean", "app-leftovers-cleanup-plan", true, true)
            .with_schema_documents(PAYLOAD_SCHEMA_DOCS)
            .with_preflight(&["doctor permissions", "doctor active-processes"])
            .with_required_execution_flag("--yes")
            .with_required_confirmation_flags(&["--allow-warning"])
            .with_macos_privacy_relevant(),
        command("purge", "project-artifact-cleanup-plan", true, true)
            .with_schema_documents(PAYLOAD_SCHEMA_DOCS)
            .with_preflight(&["config show"])
            .with_required_execution_flag("--yes")
            .with_macos_privacy_relevant(),
        command("inspect space", "inspect-space", false, true),
        command("inspect map", "inspect-map", false, true),
        command("inspect artifacts", "inspect-artifacts", false, true)
            .with_preflight(&["config show"]),
        command("inspect lint", "inspect-lint", false, false),
        command("history", "history-list", false, false),
        command("cache inspect", "cache-inventory", false, false),
        command("cache doctor", "cache-doctor", false, false),
        command("cache prune", "cache-prune-report", true, false)
            .with_required_execution_flag("--yes"),
        command("cache purge", "cache-purge-report", true, false)
            .with_required_execution_flag("--yes")
            .with_required_confirmation_flags(&["--permanent"]),
        command("config paths", "config-paths", false, false)
            .with_schema_documents(CONFIG_SCHEMA_DOCS),
        command("config show", "config-view", false, false)
            .with_schema_documents(CONFIG_SCHEMA_DOCS),
        command("config validate", "config-validation", false, false)
            .with_schema_documents(CONFIG_SCHEMA_DOCS),
        command("doctor permissions", "permissions-diagnostic", false, false)
            .with_macos_privacy_relevant(),
        command(
            "doctor active-processes",
            "active-process-diagnostic",
            false,
            false,
        ),
    ]
}

fn command(
    name: &'static str,
    payload_kind: &'static str,
    mutates_files: bool,
    ndjson: bool,
) -> CommandCapability {
    CommandCapability {
        name,
        payload_kind,
        machine_readable: true,
        ndjson,
        mutates_files,
        availability: command_availability(mutates_files),
        platforms: command_platforms(),
        schema_documents: PAYLOAD_SCHEMA_DOCS,
        preflight_commands: &[],
        required_execution_flag: None,
        required_confirmation_flags: &[],
        macos_privacy_relevant: false,
    }
}

impl CommandCapability {
    fn with_schema_documents(mut self, schema_documents: &'static [&'static str]) -> Self {
        self.schema_documents = schema_documents;
        self
    }

    fn with_preflight(mut self, preflight_commands: &'static [&'static str]) -> Self {
        self.preflight_commands = preflight_commands;
        self
    }

    fn with_required_execution_flag(mut self, flag: &'static str) -> Self {
        self.required_execution_flag = Some(flag);
        self
    }

    fn with_required_confirmation_flags(mut self, flags: &'static [&'static str]) -> Self {
        self.required_confirmation_flags = flags;
        self
    }

    fn with_macos_privacy_relevant(mut self) -> Self {
        self.macos_privacy_relevant = true;
        self
    }
}

fn command_availability(mutates_files: bool) -> CommandAvailability {
    if cleanup_execution_supported() {
        CommandAvailability::Available
    } else if mutates_files {
        CommandAvailability::PreviewOnly
    } else {
        CommandAvailability::UnsupportedPlatform
    }
}

fn cleanup_execution_supported() -> bool {
    matches!(
        Platform::current(),
        Platform::Windows | Platform::Linux | Platform::Macos
    )
}

fn command_platforms() -> &'static [&'static str] {
    if cleanup_execution_supported() {
        ALL_PLATFORMS
    } else {
        CURRENT_PLATFORM_ONLY
    }
}

fn clean_preflight_commands() -> &'static [&'static str] {
    if Platform::current() == Platform::Macos {
        MACOS_CLEAN_PREFLIGHT
    } else {
        CLEAN_PREFLIGHT
    }
}

fn print_human(report: &CapabilitiesReport) {
    println!("Rebecca CLI capabilities");
    println!("API version: {}", report.api_version);
    println!("CLI version: {}", report.cli_version);
    println!("Platform: {}", report.platform.current);
    println!(
        "Features: rules={}, windows={}, ntfs={}",
        report.features.rules, report.features.windows, report.features.ntfs
    );
    println!("Machine formats: {}", report.output_formats.join(", "));
    println!("Schema documents: {}", report.schema_documents.join(", "));
    println!("Commands:");
    for command in &report.commands {
        println!(
            "  - {} -> {}{}{}",
            command.name,
            command.payload_kind,
            if command.ndjson { " [ndjson]" } else { "" },
            if command.mutates_files {
                " [mutating]"
            } else {
                ""
            }
        );
    }
}