use anyhow::Result;
use clap::error::ErrorKind;
use clap::{CommandFactory, Parser};
use clap_complete::{Shell, generate};
use std::io;
mod apps;
mod cache;
mod cache_view;
mod capabilities;
mod catalog;
mod clean;
mod clean_view;
mod cli;
mod config_cmd;
mod history_view;
mod info;
mod inspect;
mod output;
mod progress;
mod purge;
mod purge_view;
mod render;
mod rules_cmd;
mod runtime;
mod scan;
mod schema;
mod text;
mod trash_backend;
mod tui;
mod workbench;
use cli::{
AppsCommand, CacheCommand, CatalogArgs, CatalogCommand, CleanArgs, Cli, Command,
CompletionArgs, ConfigCommand, DoctorCommand, HistoryArgs, InspectCommand, OutputMode,
PurgeArgs, RulesCommand, ScanArgs, SchemaCommand, TuiArgs,
};
use runtime::CliRuntime;
fn main() {
init_tracing();
if let Err(err) = run() {
if output::is_broken_pipe_error(&err) {
std::process::exit(0);
}
if err.downcast_ref::<output::MachineErrorRendered>().is_some() {
std::process::exit(1);
}
let cli = Cli::try_parse();
let (contract, mode) = cli
.as_ref()
.ok()
.map(|cli| (command_api_contract(&cli.command), command_output_mode(cli)))
.unwrap_or((
output::CliApiContract::v1("rebecca", "command-error"),
OutputMode::Human,
));
output::render_error(contract, 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 {
match Cli::try_parse() {
Ok(cli) => cli,
Err(err) => {
handle_cli_parse_error(err)?;
unreachable!("CLI parse error handler should exit or return an error");
}
}
};
let runtime = CliRuntime::with_ctrlc_handler()?;
match cli.command {
Command::Capabilities => capabilities::run(cli.format),
Command::Catalog(args) => run_catalog(args, cli.format),
Command::Rules { command } => match command {
RulesCommand::Validate(args) => rules_cmd::validate(
cli.format,
args.files,
args.dirs,
rules_cmd::RuleDiscoveryOptions {
max_depth: args.max_depth,
max_files: args.max_files,
},
),
RulesCommand::Import(args) => rules_cmd::import(cli.format, args.file),
RulesCommand::List => rules_cmd::list(cli.format),
RulesCommand::Enable(args) => rules_cmd::enable(cli.format, args.import_id),
RulesCommand::Disable(args) => rules_cmd::disable(cli.format, args.import_id),
RulesCommand::Remove(args) => rules_cmd::remove(cli.format, args.import_id),
},
Command::Scan(args) => run_scan(args, cli.format),
Command::Clean(args) => run_clean(args, cli.format, &runtime),
Command::Tui(args) => run_tui(args, cli.format, &runtime),
Command::Inspect { command } => run_inspect(command, cli.format, &runtime),
Command::Purge(args) => run_purge(args, cli.format, &runtime),
Command::History(args) => run_history(args, cli.format),
Command::Cache { command } => match command {
CacheCommand::Inspect { namespace } => cache::inspect(cache::CacheInspectOptions {
output_mode: cli.format,
namespace: namespace.into(),
}),
CacheCommand::Doctor => cache::doctor(cache::CacheDoctorOptions {
output_mode: cli.format,
}),
CacheCommand::Prune {
namespace,
stale_only,
limit,
dry_run,
yes,
} => cache::prune(cache::CachePruneOptions {
output_mode: cli.format,
namespace: namespace.into(),
stale_only,
limit,
dry_run,
yes,
}),
CacheCommand::Purge {
dry_run,
yes,
permanent,
} => cache::purge(cache::CachePurgeOptions {
dry_run,
output_mode: cli.format,
yes,
permanent,
}),
},
Command::Apps { command } => match command {
AppsCommand::Scan {
no_progress,
progress_detail,
scan_cache,
no_scan_cache,
exclude_paths,
} => apps::scan_with_runtime(
apps::AppsScanOptions {
output_mode: cli.format,
no_progress,
progress_detail,
scan_cache: effective_scan_cache(true, scan_cache, no_scan_cache),
exclude_paths,
},
&runtime,
),
AppsCommand::Clean {
dry_run,
yes,
no_progress,
progress_detail,
scan_cache,
no_scan_cache,
exclude_paths,
} => apps::clean_with_runtime(
apps::AppsCleanOptions {
dry_run,
output_mode: cli.format,
yes,
no_progress,
progress_detail,
scan_cache: effective_scan_cache(
workflow_is_dry_run(dry_run, yes),
scan_cache,
no_scan_cache,
),
exclude_paths,
},
&runtime,
),
},
Command::Config { command } => match command {
ConfigCommand::Paths => info::print_config_paths(cli.format),
ConfigCommand::Show(args) => config_cmd::show(cli.format, args.file),
ConfigCommand::Validate(args) => config_cmd::validate(cli.format, args.file),
},
Command::Doctor { command } => match command {
DoctorCommand::Permissions => info::print_privilege_level(cli.format),
DoctorCommand::ActiveProcesses => info::print_active_processes(cli.format),
},
Command::Schema { command } => match command {
SchemaCommand::Export(args) => schema::export(cli.format, args.document),
},
Command::Completion(args) => run_completion(args),
}
}
fn handle_cli_parse_error(err: clap::Error) -> Result<()> {
if matches!(
err.kind(),
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion
) {
err.print()?;
std::process::exit(0);
}
if let Some(mode) = detect_machine_output_mode_from_args() {
let contract = infer_command_api_contract_from_args();
let err = anyhow::Error::new(err);
output::render_error(contract, mode, &err);
return Err(output::MachineErrorRendered.into());
}
let exit_code = err.exit_code();
err.print()?;
std::process::exit(exit_code);
}
fn detect_machine_output_mode_from_args() -> Option<OutputMode> {
let mut args = std::env::args_os().skip(1);
while let Some(arg) = args.next() {
let value = arg.to_string_lossy();
if value == "--format" {
let format = args.next()?;
return output_mode_from_raw(&format.to_string_lossy());
}
if let Some(format) = value.strip_prefix("--format=") {
return output_mode_from_raw(format);
}
}
None
}
fn output_mode_from_raw(raw: &str) -> Option<OutputMode> {
match raw {
"json" => Some(OutputMode::Json),
"ndjson" => Some(OutputMode::Ndjson),
_ => None,
}
}
fn infer_command_api_contract_from_args() -> output::CliApiContract {
let tokens = positional_tokens_for_command_contract();
let command = tokens.first().map(String::as_str);
match command {
Some("catalog") => {
if matches!(tokens.get(1).map(String::as_str), Some("validate")) {
output::CliApiContract::v1("catalog validate", "catalog-validation")
} else {
output::CliApiContract::v1("catalog", "catalog")
}
}
Some("rules") => match tokens.get(1).map(String::as_str) {
Some("validate") => output::CliApiContract::v1("rules validate", "rule-validation"),
Some("import") => output::CliApiContract::v1("rules import", "rule-import"),
Some("list") => output::CliApiContract::v1("rules list", "rule-import-list"),
Some("enable") => output::CliApiContract::v1("rules enable", "rule-import-mutation"),
Some("disable") => output::CliApiContract::v1("rules disable", "rule-import-mutation"),
Some("remove") => output::CliApiContract::v1("rules remove", "rule-import-mutation"),
_ => output::CliApiContract::v1("rules", "command-error"),
},
Some("scan") => output::CliApiContract::v1("scan", "rule-catalog"),
Some("clean") => output::CliApiContract::v1("clean", "cleanup-plan"),
Some("tui") | Some("i") => output::CliApiContract::v1("tui", "terminal-workbench"),
Some("inspect") => match tokens.get(1).map(String::as_str) {
Some("space") => output::CliApiContract::v1("inspect space", "inspect-space"),
Some("map") => output::CliApiContract::v1("inspect map", "inspect-map"),
Some("artifacts") => {
output::CliApiContract::v1("inspect artifacts", "inspect-artifacts")
}
Some("lint") => output::CliApiContract::v1("inspect lint", "inspect-lint"),
_ => output::CliApiContract::v1("inspect", "command-error"),
},
Some("purge") => output::CliApiContract::v1("purge", "project-artifact-cleanup-plan"),
Some("history") => output::CliApiContract::v1("history", "history-list"),
Some("cache") => match tokens.get(1).map(String::as_str) {
Some("inspect") => output::CliApiContract::v1("cache inspect", "cache-inventory"),
Some("doctor") => output::CliApiContract::v1("cache doctor", "cache-doctor"),
Some("prune") => output::CliApiContract::v1("cache prune", "cache-prune-report"),
Some("purge") => output::CliApiContract::v1("cache purge", "cache-purge-report"),
_ => output::CliApiContract::v1("cache", "command-error"),
},
Some("apps") => match tokens.get(1).map(String::as_str) {
Some("scan") => output::CliApiContract::v1("apps scan", "app-leftovers-cleanup-plan"),
Some("clean") => output::CliApiContract::v1("apps clean", "app-leftovers-cleanup-plan"),
_ => output::CliApiContract::v1("apps", "command-error"),
},
Some("config") => match tokens.get(1).map(String::as_str) {
Some("paths") => output::CliApiContract::v1("config paths", "config-paths"),
_ => output::CliApiContract::v1("config", "command-error"),
},
Some("doctor") => match tokens.get(1).map(String::as_str) {
Some("permissions") => {
output::CliApiContract::v1("doctor permissions", "permissions-diagnostic")
}
Some("active-processes") => {
output::CliApiContract::v1("doctor active-processes", "active-process-diagnostic")
}
_ => output::CliApiContract::v1("doctor", "command-error"),
},
Some("schema") => output::CliApiContract::v1("schema export", "cli-schema"),
Some("completion") => output::CliApiContract::v1("completion", "completion-script"),
_ => output::CliApiContract::v1("rebecca", "command-error"),
}
}
fn positional_tokens_for_command_contract() -> Vec<String> {
let mut tokens = Vec::new();
let mut skip_next = false;
for arg in std::env::args_os().skip(1) {
if skip_next {
skip_next = false;
continue;
}
let value = arg.to_string_lossy();
if value == "--format" {
skip_next = true;
continue;
}
if value.starts_with("--format=") || value.starts_with('-') {
continue;
}
tokens.push(value.into_owned());
}
tokens
}
fn run_tui(args: TuiArgs, global_mode: OutputMode, runtime: &CliRuntime) -> Result<()> {
tui::run_with_runtime(
tui::TuiOptions {
output_mode: global_mode,
roots: args.roots,
scan_backend: args.scan_backend.map(Into::into),
entry_limit: args.entry_limit,
screen_reader: if args.screen_reader {
Some(true)
} else if args.visual_bars {
Some(false)
} else {
None
},
no_color: if args.no_color {
Some(true)
} else if args.color {
Some(false)
} else {
None
},
once: args.once,
replay_keys: args.replay_keys,
terminal_width: args.terminal_width,
},
runtime,
)
}
fn run_inspect(
command: InspectCommand,
global_mode: OutputMode,
runtime: &CliRuntime,
) -> Result<()> {
match command {
InspectCommand::Space(args) => inspect::space_with_runtime(
inspect::InspectSpaceOptions {
output_mode: global_mode,
no_progress: args.no_progress,
progress_detail: args.progress_detail,
scan_cache: args.scan_cache,
scan_backend: args.scan_backend,
roots: args.roots,
top_limit: args.top_limit,
diagnostic_limit: args.diagnostic_limit,
},
runtime,
),
InspectCommand::Map(args) => inspect::map_with_runtime(
inspect::InspectMapOptions {
output_mode: global_mode,
no_progress: args.no_progress,
progress_detail: args.progress_detail,
scan_backend: args.scan_backend,
roots: args.roots,
top_limit: args.top_limit,
sort: args.sort.into(),
min_logical_bytes: args.min_logical_bytes,
entry_kind: args.entry_kind.map(Into::into),
path_contains: args.path_contains,
cleanup_advice: args.cleanup_advice,
screen_reader: args.screen_reader,
full_path: args.full_path,
no_bars: args.no_bars,
bar_width: args.bar_width,
advice_status: args.advice_status.map(Into::into),
group_kinds: args.group_kinds.into_iter().map(Into::into).collect(),
group_limit: args.group_limit,
group_sort: args.group_sort.into(),
table_format: args.table_format.map(|format| match format {
cli::InspectMapTableFormatArg::Csv => inspect::InspectMapTableFormat::Csv,
cli::InspectMapTableFormatArg::Tsv => inspect::InspectMapTableFormat::Tsv,
}),
table_row_kinds: args
.table_row_kinds
.into_iter()
.map(|kind| match kind {
cli::InspectMapTableRowKindArg::Total => {
inspect::InspectMapTableRowKind::Total
}
cli::InspectMapTableRowKindArg::Root => {
inspect::InspectMapTableRowKind::Root
}
cli::InspectMapTableRowKindArg::Entry => {
inspect::InspectMapTableRowKind::Entry
}
cli::InspectMapTableRowKindArg::Group => {
inspect::InspectMapTableRowKind::Group
}
})
.collect(),
diagnostic_limit: args.diagnostic_limit,
max_depth: args.max_depth,
},
runtime,
),
InspectCommand::Artifacts(args) => inspect::artifacts_with_runtime(
inspect::InspectArtifactsOptions {
output_mode: global_mode,
no_progress: args.no_progress,
progress_detail: args.progress_detail,
scan_cache: args.scan_cache,
roots: args.roots,
max_depth: args.max_depth,
min_age_days: args.min_age_days,
reclaim_limit_bytes: args.reclaim_limit_bytes,
artifacts: args.artifacts,
exclude_paths: args.exclude_paths,
command: "inspect artifacts",
},
runtime,
),
InspectCommand::Lint(args) => inspect::lint_with_runtime(
inspect::InspectLintOptions {
output_mode: global_mode,
roots: args.roots,
reference_roots: args.reference_roots,
exclude_paths: args.exclude_paths,
large_file_threshold_bytes: args.large_file_threshold_bytes,
top_limit: args.top_limit,
},
runtime,
),
}
}
fn run_catalog(args: CatalogArgs, global_mode: OutputMode) -> Result<()> {
let CatalogArgs {
command,
kind,
categories,
rules,
artifacts,
warnings,
safety_level,
platform,
} = args;
if let Some(CatalogCommand::Validate) = command {
return catalog::validate(global_mode);
}
catalog::run(catalog::CatalogOptions {
output_mode: global_mode,
kind: kind.map(Into::into),
categories,
rules,
artifacts,
warnings,
safety_level: safety_level.map(Into::into),
platform: platform.map(Into::into),
})
}
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, runtime: &CliRuntime) -> Result<()> {
let CleanArgs {
dry_run,
yes,
selection,
execution,
risk,
} = args;
let is_dry_run = workflow_is_dry_run(dry_run, yes);
clean::run_with_runtime(
clean::CleanOptions {
dry_run,
output_mode: global_mode,
yes,
no_progress: execution.no_progress,
progress_detail: execution.progress_detail,
scan_cache: effective_scan_cache(
is_dry_run,
execution.scan_cache,
execution.no_scan_cache,
),
scan_backend: execution.scan_backend.into(),
categories: selection.categories,
rules: selection.rules,
exclude_paths: execution.exclude_paths,
allow_moderate: risk.allow_moderate,
allow_risky: risk.allow_risky,
allow_warnings: risk.allow_warnings,
},
runtime,
)
}
fn run_purge(args: PurgeArgs, global_mode: OutputMode, runtime: &CliRuntime) -> Result<()> {
let PurgeArgs {
dry_run,
yes,
no_progress,
progress_detail,
scan_cache,
no_scan_cache,
roots,
max_depth,
min_age_days,
reclaim_limit_bytes,
artifacts,
exclude_paths,
} = args;
purge::run_with_runtime(
purge::PurgeOptions {
dry_run,
output_mode: global_mode,
yes,
no_progress,
progress_detail,
scan_cache: effective_scan_cache(
workflow_is_dry_run(dry_run, yes),
scan_cache,
no_scan_cache,
),
roots,
max_depth,
min_age_days,
reclaim_limit_bytes,
artifacts,
exclude_paths,
},
runtime,
)
}
fn workflow_is_dry_run(dry_run: bool, yes: bool) -> bool {
dry_run || !yes
}
fn effective_scan_cache(is_dry_run: bool, scan_cache: bool, no_scan_cache: bool) -> bool {
!no_scan_cache && (scan_cache || is_dry_run)
}
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_api_contract(command: &Command) -> output::CliApiContract {
match command {
Command::Capabilities => output::CliApiContract::v1("capabilities", "capabilities"),
Command::Catalog(args) => {
if matches!(args.command.as_ref(), Some(CatalogCommand::Validate)) {
output::CliApiContract::v1("catalog validate", "catalog-validation")
} else {
output::CliApiContract::v1("catalog", "catalog")
}
}
Command::Rules { command } => match command {
RulesCommand::Validate(_) => {
output::CliApiContract::v1("rules validate", "rule-validation")
}
RulesCommand::Import(_) => output::CliApiContract::v1("rules import", "rule-import"),
RulesCommand::List => output::CliApiContract::v1("rules list", "rule-import-list"),
RulesCommand::Enable(_) => {
output::CliApiContract::v1("rules enable", "rule-import-mutation")
}
RulesCommand::Disable(_) => {
output::CliApiContract::v1("rules disable", "rule-import-mutation")
}
RulesCommand::Remove(_) => {
output::CliApiContract::v1("rules remove", "rule-import-mutation")
}
},
Command::Scan(_) => output::CliApiContract::v1("scan", "rule-catalog"),
Command::Clean(_) => output::CliApiContract::v1("clean", "cleanup-plan"),
Command::Tui(_) => output::CliApiContract::v1("tui", "terminal-workbench"),
Command::Inspect { command } => match command {
InspectCommand::Space(_) => {
output::CliApiContract::v1("inspect space", "inspect-space")
}
InspectCommand::Map(_) => output::CliApiContract::v1("inspect map", "inspect-map"),
InspectCommand::Artifacts(_) => {
output::CliApiContract::v1("inspect artifacts", "inspect-artifacts")
}
InspectCommand::Lint(_) => output::CliApiContract::v1("inspect lint", "inspect-lint"),
},
Command::Purge(_) => output::CliApiContract::v1("purge", "project-artifact-cleanup-plan"),
Command::History(_) => output::CliApiContract::v1("history", "history-list"),
Command::Cache { command } => match command {
CacheCommand::Inspect { .. } => {
output::CliApiContract::v1("cache inspect", "cache-inventory")
}
CacheCommand::Doctor => output::CliApiContract::v1("cache doctor", "cache-doctor"),
CacheCommand::Prune { .. } => {
output::CliApiContract::v1("cache prune", "cache-prune-report")
}
CacheCommand::Purge { .. } => {
output::CliApiContract::v1("cache purge", "cache-purge-report")
}
},
Command::Apps { command } => match command {
AppsCommand::Scan { .. } => {
output::CliApiContract::v1("apps scan", "app-leftovers-cleanup-plan")
}
AppsCommand::Clean { .. } => {
output::CliApiContract::v1("apps clean", "app-leftovers-cleanup-plan")
}
},
Command::Config { command } => match command {
ConfigCommand::Paths => output::CliApiContract::v1("config paths", "config-paths"),
ConfigCommand::Show(_) => output::CliApiContract::v1("config show", "config-view"),
ConfigCommand::Validate(_) => {
output::CliApiContract::v1("config validate", "config-validation")
}
},
Command::Doctor { command } => match command {
DoctorCommand::Permissions => {
output::CliApiContract::v1("doctor permissions", "permissions-diagnostic")
}
DoctorCommand::ActiveProcesses => {
output::CliApiContract::v1("doctor active-processes", "active-process-diagnostic")
}
},
Command::Schema { command } => match command {
SchemaCommand::Export(_) => output::CliApiContract::v1("schema export", "cli-schema"),
},
Command::Completion(_) => output::CliApiContract::v1("completion", "completion-script"),
}
}