1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
// 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)
}