use std::num::NonZeroUsize;
use std::path::PathBuf;
use clap::{Args, Parser, Subcommand, ValueEnum, ValueHint};
pub const DEFAULT_RULE_VALIDATE_MAX_DEPTH: usize = 8;
pub const DEFAULT_RULE_VALIDATE_MAX_FILES: usize = 512;
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum OutputMode {
Human,
Json,
Ndjson,
}
impl OutputMode {
pub(crate) fn is_human(self) -> bool {
matches!(self, Self::Human)
}
pub(crate) fn is_ndjson(self) -> bool {
matches!(self, Self::Ndjson)
}
}
impl std::fmt::Display for OutputMode {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let label = match self {
Self::Human => "human",
Self::Json => "json",
Self::Ndjson => "ndjson",
};
formatter.write_str(label)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)]
pub enum ProgressDetail {
#[default]
Target,
File,
}
impl ProgressDetail {
pub(crate) fn includes_file_events(self) -> bool {
matches!(self, Self::File)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)]
pub enum ScanBackendArg {
#[default]
PortableRecursive,
WindowsNative,
WindowsNtfsMftExperimental,
}
impl From<ScanBackendArg> for rebecca::core::scan::ScanBackendKind {
fn from(value: ScanBackendArg) -> Self {
match value {
ScanBackendArg::PortableRecursive => Self::PortableRecursive,
ScanBackendArg::WindowsNative => Self::WindowsNative,
ScanBackendArg::WindowsNtfsMftExperimental => Self::WindowsNtfsMftExperimental,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum DiskMapGroupKindArg {
Type,
Extension,
Depth,
Age,
}
impl From<DiskMapGroupKindArg> for rebecca::core::disk_map::DiskMapGroupKind {
fn from(value: DiskMapGroupKindArg) -> Self {
match value {
DiskMapGroupKindArg::Type => Self::Type,
DiskMapGroupKindArg::Extension => Self::Extension,
DiskMapGroupKindArg::Depth => Self::Depth,
DiskMapGroupKindArg::Age => Self::Age,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum DiskMapEntryKindArg {
File,
Directory,
Other,
}
impl From<DiskMapEntryKindArg> for rebecca::core::disk_map::DiskMapEntryKind {
fn from(value: DiskMapEntryKindArg) -> Self {
match value {
DiskMapEntryKindArg::File => Self::File,
DiskMapEntryKindArg::Directory => Self::Directory,
DiskMapEntryKindArg::Other => Self::Other,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum CleanupAdviceStatusArg {
Cleanable,
MaybeCleanable,
ContainsCleanable,
Protected,
Unknown,
}
impl From<CleanupAdviceStatusArg> for rebecca::core::cleanup_advice::CleanupAdviceStatus {
fn from(value: CleanupAdviceStatusArg) -> Self {
match value {
CleanupAdviceStatusArg::Cleanable => Self::Cleanable,
CleanupAdviceStatusArg::MaybeCleanable => Self::MaybeCleanable,
CleanupAdviceStatusArg::ContainsCleanable => Self::ContainsCleanable,
CleanupAdviceStatusArg::Protected => Self::Protected,
CleanupAdviceStatusArg::Unknown => Self::Unknown,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)]
pub enum DiskMapSortArg {
#[default]
Logical,
Allocated,
Files,
Unique,
}
impl From<DiskMapSortArg> for rebecca::core::disk_map::DiskMapSortField {
fn from(value: DiskMapSortArg) -> Self {
match value {
DiskMapSortArg::Logical => Self::Logical,
DiskMapSortArg::Allocated => Self::Allocated,
DiskMapSortArg::Files => Self::Files,
DiskMapSortArg::Unique => Self::Unique,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum InspectMapTableFormatArg {
Csv,
Tsv,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum InspectMapTableRowKindArg {
Total,
Root,
Entry,
Group,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum CatalogKindArg {
CleanupRule,
ProjectArtifact,
Warning,
SafetyCategory,
ActionKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum PlatformArg {
Windows,
Linux,
Macos,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Subcommand)]
pub enum CatalogCommand {
Validate,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum SchemaDocumentArg {
Envelope,
Event,
Error,
Payloads,
Config,
CleanerManifestV1,
}
impl From<CatalogKindArg> for rebecca::core::catalog::CatalogItemKind {
fn from(kind: CatalogKindArg) -> Self {
match kind {
CatalogKindArg::CleanupRule => Self::CleanupRule,
CatalogKindArg::ProjectArtifact => Self::ProjectArtifact,
CatalogKindArg::Warning => Self::Warning,
CatalogKindArg::SafetyCategory => Self::SafetyCategory,
CatalogKindArg::ActionKind => Self::ActionKind,
}
}
}
impl From<PlatformArg> for rebecca::core::Platform {
fn from(platform: PlatformArg) -> Self {
match platform {
PlatformArg::Windows => Self::Windows,
PlatformArg::Linux => Self::Linux,
PlatformArg::Macos => Self::Macos,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum SafetyLevelArg {
Safe,
Moderate,
Risky,
Dangerous,
}
impl From<SafetyLevelArg> for rebecca::core::SafetyLevel {
fn from(level: SafetyLevelArg) -> Self {
match level {
SafetyLevelArg::Safe => Self::Safe,
SafetyLevelArg::Moderate => Self::Moderate,
SafetyLevelArg::Risky => Self::Risky,
SafetyLevelArg::Dangerous => Self::Dangerous,
}
}
}
#[derive(Debug, Parser)]
#[command(
name = "rebecca",
version,
about = "Cross-platform cleanup CLI",
subcommand_required = true,
arg_required_else_help = true
)]
pub struct Cli {
#[arg(
long,
value_enum,
default_value_t = OutputMode::Human,
global = true
)]
pub format: OutputMode,
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Subcommand)]
pub enum Command {
Capabilities,
Catalog(CatalogArgs),
Rules {
#[command(subcommand)]
command: RulesCommand,
},
Scan(ScanArgs),
Clean(CleanArgs),
#[command(visible_alias = "i")]
Tui(TuiArgs),
Inspect {
#[command(subcommand)]
command: InspectCommand,
},
Purge(PurgeArgs),
History(HistoryArgs),
Cache {
#[command(subcommand)]
command: CacheCommand,
},
Apps {
#[command(subcommand)]
command: AppsCommand,
},
Config {
#[command(subcommand)]
command: ConfigCommand,
},
Doctor {
#[command(subcommand)]
command: DoctorCommand,
},
Schema {
#[command(subcommand)]
command: SchemaCommand,
},
Completion(CompletionArgs),
}
#[derive(Debug, Subcommand)]
pub enum InspectCommand {
Space(InspectSpaceArgs),
Map(InspectMapArgs),
Artifacts(InspectArtifactsArgs),
Lint(InspectLintArgs),
}
#[derive(Debug, Args)]
pub struct InspectSpaceArgs {
#[arg(long)]
pub no_progress: bool,
#[arg(long, value_enum, default_value_t = ProgressDetail::Target)]
pub progress_detail: ProgressDetail,
#[arg(long)]
pub scan_cache: bool,
#[arg(long = "scan-backend", value_enum, default_value_t = ScanBackendArg::PortableRecursive)]
pub scan_backend: ScanBackendArg,
#[arg(long = "root", value_name = "PATH", value_hint = ValueHint::DirPath)]
pub roots: Vec<PathBuf>,
#[arg(long = "top", value_name = "N", default_value_t = 10)]
pub top_limit: usize,
#[arg(long = "diagnostic-limit", value_name = "N", default_value_t = rebecca::core::inspect::DEFAULT_SPACE_INSIGHT_DIAGNOSTIC_LIMIT)]
pub diagnostic_limit: usize,
}
#[derive(Debug, Args)]
pub struct InspectMapArgs {
#[arg(long)]
pub no_progress: bool,
#[arg(long, value_enum, default_value_t = ProgressDetail::Target)]
pub progress_detail: ProgressDetail,
#[arg(long = "scan-backend", value_enum, default_value_t = ScanBackendArg::PortableRecursive)]
pub scan_backend: ScanBackendArg,
#[arg(long = "root", value_name = "PATH", value_hint = ValueHint::AnyPath)]
pub roots: Vec<PathBuf>,
#[arg(long = "top", value_name = "N", default_value_t = rebecca::core::disk_map::DEFAULT_DISK_MAP_TOP_LIMIT)]
pub top_limit: usize,
#[arg(long = "sort", value_enum, default_value_t = DiskMapSortArg::Logical)]
pub sort: DiskMapSortArg,
#[arg(long = "min-logical-bytes", value_name = "BYTES")]
pub min_logical_bytes: Option<u64>,
#[arg(long = "entry-kind", value_enum, value_name = "KIND")]
pub entry_kind: Option<DiskMapEntryKindArg>,
#[arg(long = "path-contains", value_name = "TEXT")]
pub path_contains: Option<String>,
#[arg(long = "cleanup-advice")]
pub cleanup_advice: bool,
#[arg(long = "screen-reader")]
pub screen_reader: bool,
#[arg(long = "full-path")]
pub full_path: bool,
#[arg(long = "no-bars")]
pub no_bars: bool,
#[arg(long = "bar-width", value_name = "COLUMNS")]
pub bar_width: Option<usize>,
#[arg(long = "advice-status", value_enum, value_name = "STATUS")]
pub advice_status: Option<CleanupAdviceStatusArg>,
#[arg(long = "group-by", value_enum)]
pub group_kinds: Vec<DiskMapGroupKindArg>,
#[arg(long = "group-limit", value_name = "N", default_value_t = rebecca::core::disk_map::DEFAULT_DISK_MAP_GROUP_LIMIT)]
pub group_limit: usize,
#[arg(long = "group-sort", value_enum, default_value_t = DiskMapSortArg::Logical)]
pub group_sort: DiskMapSortArg,
#[arg(long = "table", value_enum, value_name = "FORMAT")]
pub table_format: Option<InspectMapTableFormatArg>,
#[arg(long = "table-row", value_enum, value_name = "KIND")]
pub table_row_kinds: Vec<InspectMapTableRowKindArg>,
#[arg(long = "diagnostic-limit", value_name = "N", default_value_t = rebecca::core::disk_map::DEFAULT_DISK_MAP_DIAGNOSTIC_LIMIT)]
pub diagnostic_limit: usize,
#[arg(long = "max-depth", value_name = "N")]
pub max_depth: Option<usize>,
}
#[derive(Debug, Args)]
pub struct InspectArtifactsArgs {
#[arg(long)]
pub no_progress: bool,
#[arg(long, value_enum, default_value_t = ProgressDetail::Target)]
pub progress_detail: ProgressDetail,
#[arg(long)]
pub scan_cache: bool,
#[arg(long = "root", value_name = "PATH", value_hint = ValueHint::DirPath)]
pub roots: Vec<PathBuf>,
#[arg(long, value_name = "N")]
pub max_depth: Option<usize>,
#[arg(long, alias = "older-than-days", value_name = "DAYS")]
pub min_age_days: Option<u64>,
#[arg(long, value_name = "BYTES")]
pub reclaim_limit_bytes: Option<u64>,
#[arg(long = "artifact", value_name = "ARTIFACT")]
pub artifacts: Vec<String>,
#[arg(long = "exclude", value_name = "PATH", value_hint = ValueHint::AnyPath)]
pub exclude_paths: Vec<PathBuf>,
}
#[derive(Debug, Args)]
pub struct InspectLintArgs {
#[arg(long = "root", value_name = "PATH", value_hint = ValueHint::DirPath)]
pub roots: Vec<PathBuf>,
#[arg(long = "reference", value_name = "PATH", value_hint = ValueHint::DirPath)]
pub reference_roots: Vec<PathBuf>,
#[arg(long = "exclude", value_name = "PATH", value_hint = ValueHint::AnyPath)]
pub exclude_paths: Vec<PathBuf>,
#[arg(long, value_name = "BYTES", default_value_t = rebecca::core::lint::DEFAULT_LARGE_FILE_THRESHOLD_BYTES)]
pub large_file_threshold_bytes: u64,
#[arg(long = "top", value_name = "N", default_value_t = rebecca::core::lint::DEFAULT_LINT_TOP_LIMIT)]
pub top_limit: usize,
}
#[derive(Debug, Args)]
pub struct CatalogArgs {
#[command(subcommand)]
pub command: Option<CatalogCommand>,
#[arg(long, value_enum)]
pub kind: Option<CatalogKindArg>,
#[arg(long = "category")]
pub categories: Vec<String>,
#[arg(long = "rule")]
pub rules: Vec<String>,
#[arg(long = "artifact")]
pub artifacts: Vec<String>,
#[arg(long = "warning")]
pub warnings: Vec<String>,
#[arg(long = "safety-level", value_enum)]
pub safety_level: Option<SafetyLevelArg>,
#[arg(long, value_enum)]
pub platform: Option<PlatformArg>,
}
#[derive(Debug, Subcommand)]
pub enum RulesCommand {
Validate(RulesValidateArgs),
Import(RulesImportArgs),
List,
Enable(RulesImportIdArgs),
Disable(RulesImportIdArgs),
Remove(RulesImportIdArgs),
}
#[derive(Debug, Args)]
pub struct RulesValidateArgs {
#[arg(long = "file", value_name = "PATH", value_hint = ValueHint::FilePath)]
pub files: Vec<PathBuf>,
#[arg(long = "dir", value_name = "PATH", value_hint = ValueHint::DirPath)]
pub dirs: Vec<PathBuf>,
#[arg(long = "max-depth", value_name = "N", default_value_t = DEFAULT_RULE_VALIDATE_MAX_DEPTH)]
pub max_depth: usize,
#[arg(long = "max-files", value_name = "N", default_value_t = DEFAULT_RULE_VALIDATE_MAX_FILES)]
pub max_files: usize,
}
#[derive(Debug, Args)]
pub struct RulesImportArgs {
#[arg(long = "file", value_name = "PATH", value_hint = ValueHint::FilePath)]
pub file: PathBuf,
}
#[derive(Debug, Args)]
pub struct RulesImportIdArgs {
#[arg(value_name = "IMPORT_ID")]
pub import_id: String,
}
#[derive(Debug, Args)]
pub struct ScanArgs {
#[arg(long = "category")]
pub categories: Vec<String>,
#[arg(long = "rule")]
pub rules: Vec<String>,
}
#[derive(Debug, Args)]
pub struct CleanupSelectionArgs {
#[arg(long = "category")]
pub categories: Vec<String>,
#[arg(long = "rule")]
pub rules: Vec<String>,
}
#[derive(Debug, Args)]
pub struct CleanupExecutionArgs {
#[arg(long)]
pub no_progress: bool,
#[arg(long, value_enum, default_value_t = ProgressDetail::Target)]
pub progress_detail: ProgressDetail,
#[arg(long)]
pub scan_cache: bool,
#[arg(long, conflicts_with = "scan_cache")]
pub no_scan_cache: bool,
#[arg(long = "scan-backend", value_enum, default_value_t = ScanBackendArg::PortableRecursive)]
pub scan_backend: ScanBackendArg,
#[arg(long = "exclude", value_name = "PATH", value_hint = ValueHint::AnyPath)]
pub exclude_paths: Vec<PathBuf>,
}
#[derive(Debug, Args)]
pub struct RiskArgs {
#[arg(long)]
pub allow_moderate: bool,
#[arg(long)]
pub allow_risky: bool,
#[arg(long = "allow-warning", value_name = "WARNING")]
pub allow_warnings: Vec<String>,
}
#[derive(Debug, Args)]
pub struct CleanArgs {
#[arg(short = 'n', long)]
pub dry_run: bool,
#[arg(long)]
pub yes: bool,
#[command(flatten)]
pub selection: CleanupSelectionArgs,
#[command(flatten)]
pub execution: CleanupExecutionArgs,
#[command(flatten)]
pub risk: RiskArgs,
}
#[derive(Debug, Args)]
pub struct TuiArgs {
#[arg(long = "root", value_name = "PATH", value_hint = ValueHint::AnyPath)]
pub roots: Vec<PathBuf>,
#[arg(long = "scan-backend", value_enum)]
pub scan_backend: Option<ScanBackendArg>,
#[arg(long = "entry-limit", value_name = "N")]
pub entry_limit: Option<usize>,
#[arg(long = "screen-reader", conflicts_with = "visual_bars")]
pub screen_reader: bool,
#[arg(long = "visual-bars", conflicts_with = "screen_reader")]
pub visual_bars: bool,
#[arg(long = "no-color", conflicts_with = "color")]
pub no_color: bool,
#[arg(long = "color", conflicts_with = "no_color")]
pub color: bool,
#[arg(long, hide = true)]
pub once: bool,
#[arg(long = "replay-keys", value_name = "KEYS", hide = true)]
pub replay_keys: Option<String>,
#[arg(
long = "terminal-width",
value_name = "COLUMNS",
default_value_t = 120,
hide = true
)]
pub terminal_width: usize,
}
#[derive(Debug, Args)]
pub struct PurgeArgs {
#[arg(short = 'n', long)]
pub dry_run: bool,
#[arg(long)]
pub yes: bool,
#[arg(long)]
pub no_progress: bool,
#[arg(long, value_enum, default_value_t = ProgressDetail::Target)]
pub progress_detail: ProgressDetail,
#[arg(long)]
pub scan_cache: bool,
#[arg(long, conflicts_with = "scan_cache")]
pub no_scan_cache: bool,
#[arg(long = "root", value_name = "PATH", value_hint = ValueHint::DirPath)]
pub roots: Vec<PathBuf>,
#[arg(long, value_name = "N")]
pub max_depth: Option<usize>,
#[arg(long, alias = "older-than-days", value_name = "DAYS")]
pub min_age_days: Option<u64>,
#[arg(long, value_name = "BYTES")]
pub reclaim_limit_bytes: Option<u64>,
#[arg(long = "artifact", value_name = "ARTIFACT")]
pub artifacts: Vec<String>,
#[arg(long = "exclude", value_name = "PATH", value_hint = ValueHint::AnyPath)]
pub exclude_paths: Vec<PathBuf>,
}
#[derive(Debug, Args)]
pub struct HistoryArgs {
#[arg(long)]
pub limit: Option<NonZeroUsize>,
}
#[derive(Debug, Subcommand)]
pub enum CacheCommand {
Inspect {
#[arg(long, value_enum, default_value_t = CacheNamespaceArg::All)]
namespace: CacheNamespaceArg,
},
Doctor,
Prune {
#[arg(long, value_enum, default_value_t = CacheNamespaceArg::All)]
namespace: CacheNamespaceArg,
#[arg(long)]
stale_only: bool,
#[arg(long, value_name = "N")]
limit: Option<NonZeroUsize>,
#[arg(long)]
dry_run: bool,
#[arg(long)]
yes: bool,
},
Purge {
#[arg(long)]
dry_run: bool,
#[arg(long)]
yes: bool,
#[arg(long, requires = "yes", conflicts_with = "dry_run")]
permanent: bool,
},
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)]
pub enum CacheNamespaceArg {
#[default]
All,
ScanCache,
NtfsVolumeIndex,
}
impl From<CacheNamespaceArg> for rebecca::core::cache::CacheNamespace {
fn from(namespace: CacheNamespaceArg) -> Self {
match namespace {
CacheNamespaceArg::All => Self::All,
CacheNamespaceArg::ScanCache => Self::ScanCache,
CacheNamespaceArg::NtfsVolumeIndex => Self::NtfsVolumeIndex,
}
}
}
#[derive(Debug, Subcommand)]
pub enum AppsCommand {
Scan {
#[arg(long)]
no_progress: bool,
#[arg(long, value_enum, default_value_t = ProgressDetail::Target)]
progress_detail: ProgressDetail,
#[arg(long)]
scan_cache: bool,
#[arg(long, conflicts_with = "scan_cache")]
no_scan_cache: bool,
#[arg(long = "exclude", value_name = "PATH", value_hint = ValueHint::AnyPath)]
exclude_paths: Vec<PathBuf>,
},
Clean {
#[arg(short = 'n', long)]
dry_run: bool,
#[arg(long)]
yes: bool,
#[arg(long)]
no_progress: bool,
#[arg(long, value_enum, default_value_t = ProgressDetail::Target)]
progress_detail: ProgressDetail,
#[arg(long)]
scan_cache: bool,
#[arg(long, conflicts_with = "scan_cache")]
no_scan_cache: bool,
#[arg(long = "exclude", value_name = "PATH", value_hint = ValueHint::AnyPath)]
exclude_paths: Vec<PathBuf>,
},
}
#[derive(Debug, Subcommand)]
pub enum ConfigCommand {
Paths,
Show(ConfigFileArgs),
Validate(ConfigFileArgs),
}
#[derive(Debug, Args)]
pub struct ConfigFileArgs {
#[arg(long = "file", value_name = "PATH", value_hint = ValueHint::FilePath)]
pub file: Option<PathBuf>,
}
#[derive(Debug, Subcommand)]
pub enum DoctorCommand {
Permissions,
ActiveProcesses,
}
#[derive(Debug, Subcommand)]
pub enum SchemaCommand {
Export(SchemaExportArgs),
}
#[derive(Debug, Args)]
pub struct SchemaExportArgs {
#[arg(long = "document", value_enum, default_value_t = SchemaDocumentArg::Payloads)]
pub document: SchemaDocumentArg,
}
#[derive(Debug, Args)]
pub struct CompletionArgs {
#[arg(value_enum)]
pub shell: Option<clap_complete::Shell>,
}