//! ocd entry point. mod cache; mod cli; mod client; mod config; mod error; mod fetch; mod model; mod render; mod scrape; mod timefmt; use std::process::ExitCode; use clap::Parser; use cli::{CacheAction, Cli, Command, ConfigAction, RawFormat}; use model::{AccountReport, ReportStatus}; fn main() -> ExitCode { let cli = Cli::parse(); let rt = match tokio::runtime::Builder::new_multi_thread() .enable_all() .build() { Ok(rt) => rt, Err(e) => { eprintln!("ocd: could not start runtime: {e}"); return ExitCode::from(2); } }; init_tracing(cli.verbose); let result: anyhow::Result = rt.block_on(async { run(cli).await }); match result { Ok(code) => code, Err(e) => { eprintln!("ocd: {e:#}"); ExitCode::from(2) } } } async fn run(cli: Cli) -> anyhow::Result { let command = cli.command.clone().unwrap_or(Command::Pretty); match command { Command::Pretty => { let (cfg, opts) = load_and_options(&cli)?; let reports = fetch::run(&cfg, &opts).await?; print_summary(&reports, !cli.quiet, || { render::render_pretty(&reports, cli.ascii, cli.no_color || !stdout_is_tty()) })?; Ok(exit_code(&reports)) } Command::Raw { format } => { let (cfg, opts) = load_and_options(&cli)?; let reports = fetch::run(&cfg, &opts).await?; print_summary(&reports, !cli.quiet, || match format { RawFormat::Json => render::raw::render_json(&reports, true), RawFormat::Plain => render::raw::render_plain(&reports), })?; Ok(exit_code(&reports)) } Command::Fetch => { let (cfg, opts) = load_and_options(&cli)?; let reports = fetch::run(&cfg, &opts).await?; let ok = reports .iter() .filter(|r| matches!(r.status, ReportStatus::Ok | ReportStatus::Cached)) .count(); tracing::info!(ok, total = reports.len(), "fetch done"); Ok(exit_code(&reports)) } Command::Cache { action } => match action { CacheAction::Ls => { let entries = cache::list(); if entries.is_empty() { println!("cache is empty"); } else { println!("account\tworkspace"); for (a, w) in entries { println!("{a}\t{w}"); } } Ok(ExitCode::SUCCESS) } CacheAction::Clear => { let n = cache::clear_all()?; println!("cleared {n} entries"); Ok(ExitCode::SUCCESS) } }, Command::Config { action } => match action { ConfigAction::Init { path } => { let path = path.unwrap_or_else(|| std::path::PathBuf::from("ocd.toml")); if path.exists() { anyhow::bail!("refusing to overwrite existing {}", path.display()); } if let Some(p) = path.parent() { std::fs::create_dir_all(p)?; } std::fs::write(&path, config::TEMPLATE)?; println!("wrote {}", path.display()); Ok(ExitCode::SUCCESS) } ConfigAction::Check => { let path = config::discover(cli.config.as_deref())?; let cfg = config::load(&path)?; println!("config ok: {}", path.display()); for a in &cfg.account { let workspaces = a.workspace_ids(); println!( " account \"{}\": {} workspace(s): {}", a.name, workspaces.len(), workspaces.join(", ") ); } Ok(ExitCode::SUCCESS) } }, } } /// Load the config and resolve the fetch options from CLI flags plus config /// defaults. fn load_and_options(cli: &Cli) -> anyhow::Result<(config::Config, fetch::FetchOptions)> { let path = config::discover(cli.config.as_deref())?; let cfg = config::load(&path)?; let user_agent = cli .user_agent .clone() .unwrap_or_else(|| format!("ocd/{}", env!("CARGO_PKG_VERSION"))); let ttl = cli.ttl.or(cfg.default_ttl_secs).unwrap_or(180); let opts = fetch::FetchOptions { ttl_secs: ttl, timeout_secs: cli.timeout_secs, retries: cli.retries, force_refresh: cli.force_refresh, no_cache: cli.no_cache, workspace_pace_ms: cli.pace_ms, account_filter: cli.account.clone().unwrap_or_default(), workspace_filter: cli.workspace.clone().unwrap_or_default(), user_agent, }; Ok((cfg, opts)) } /// Print the rendered output unless quiet. The renderer runs lazily so quiet /// mode does not pay for it. fn print_summary( _reports: &[AccountReport], emit: bool, render: impl FnOnce() -> String, ) -> anyhow::Result<()> { if emit { print!("{}", render()); } Ok(()) } /// Exit 0 if at least one account is Ok or Cached. Exit 1 if all failed. fn exit_code(reports: &[AccountReport]) -> ExitCode { let any_ok = reports .iter() .any(|r| matches!(r.status, ReportStatus::Ok | ReportStatus::Cached)); if any_ok { ExitCode::SUCCESS } else { ExitCode::from(1) } } fn stdout_is_tty() -> bool { use std::io::IsTerminal; std::io::stdout().is_terminal() } fn init_tracing(verbose: u8) { use tracing_subscriber::EnvFilter; let level = match verbose { 0 => "warn", 1 => "info", 2 => "debug", _ => "trace", }; let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(level)); tracing_subscriber::fmt() .with_env_filter(filter) .with_target(false) .with_writer(std::io::stderr) .init(); }