openlatch-provider 0.2.1

Self-service onboarding CLI + runtime daemon for OpenLatch Editors and Providers
// Binary entry point. `anyhow` is allowed only here; library code uses
// `thiserror` per `.claude/rules/error-handling.md`.

use anyhow::Result;
use openlatch_provider as lib;
use std::process::ExitCode;
use std::time::Instant;

#[tokio::main]
async fn main() -> Result<ExitCode> {
    // Restart-loop rollback (P3.T2a). Runs BEFORE logging/telemetry init so a
    // crashing release that rolls back doesn't pull tracing or the sentry
    // panic hook into an inconsistent state. Uses `eprintln!` only — no
    // tracing macros, no telemetry calls.
    let rollback_event = if lib::update::should_rollback() {
        match lib::update::rollback_from_bak() {
            Ok(()) => {
                eprintln!("[openlatch-provider] rolled back update due to supervisor restart loop");
                Some(true)
            }
            Err(e) => {
                eprintln!("[openlatch-provider] rollback failed: {e}");
                Some(false)
            }
        }
    } else {
        None
    };

    // Bootstrap config + machine_id (telemetry identity). Failures here are
    // non-fatal — telemetry just stays disabled if the config can't be read.
    let cfg = lib::config::Config::load().unwrap_or_default();
    let machine_id = lib::config::machine_id_or_init().ok();

    // Sentry first — its panic hook needs to be installed before anything
    // that might panic. Hold the guard for the lifetime of `main`.
    let _sentry_guard = lib::telemetry::sentry::init_if_enabled(&cfg);

    // PostHog handle — opt-in. When consent is missing or the baked key is
    // empty, this is a no-op handle so `capture_global` is free.
    let provider_dir = lib::config::provider_dir();
    let handle = match machine_id.as_ref() {
        Some(id) => lib::telemetry::init(&provider_dir, id.clone(), false),
        None => lib::telemetry::TelemetryHandle::disabled("mach_unknown".into(), false),
    };
    lib::telemetry::install_global(handle.clone());

    // Deferred-emit the rollback signal (telemetry is up now). The detail
    // lives in tracing because the 5-event auto-update telemetry catalog
    // models rolled-back as a property on `update_completed`, not as its
    // own posthog event (per `.claude/rules/auto-update.md`).
    if let Some(success) = rollback_event {
        if success {
            tracing::info!(
                target: "update",
                "supervisor-restart-loop rollback executed at startup"
            );
        } else {
            tracing::error!(
                target: "update",
                "supervisor-restart-loop rollback failed; running binary may be the broken release"
            );
        }
    }

    // Parse CLI first so we can hand the global flags to the tracing
    // subscriber before any command runs. Without this, every `info!` /
    // `warn!` / `debug!` call in the crate is a silent no-op — the `listen`
    // daemon would print its bind banner and go dark.
    let cli = lib::cli::parse_cli();
    let g = lib::cli::GlobalArgs::from_cli(&cli);
    lib::observability::init(&g);

    // Run the requested command and surface the resulting exit code.
    let started = Instant::now();
    let result = lib::cli::dispatch_cli(cli).await;
    let duration_ms = started.elapsed().as_millis() as u64;

    let exit = match &result {
        Ok(()) => ExitCode::SUCCESS,
        Err(err) => {
            eprintln!("{err}");
            if let Some(suggestion) = &err.suggestion {
                eprintln!("\n  {} {}", lib::ui::color::ARROW, suggestion);
            }
            // Fire-and-forget telemetry for the error code.
            lib::telemetry::capture_global(lib::telemetry::Event::error_emitted(
                err.code.code,
                "cli",
            ));
            ExitCode::from(i32::from(err.exit_code()) as u8)
        }
    };

    // The dispatcher records the per-command name in its own `cli_command_invoked`
    // event (see cli/mod.rs); here we only stamp the wallclock duration as a
    // safety net so the metric is never missing.
    lib::telemetry::capture_global(lib::telemetry::Event::cli_command_invoked(
        "<global>",
        "auto",
        match &result {
            Ok(()) => 0,
            Err(e) => i32::from(e.exit_code()),
        },
        duration_ms,
    ));

    lib::telemetry::shutdown(handle).await;
    Ok(exit)
}