use crate::cli::commands::lifecycle;
use crate::cli::output::{OutputConfig, OutputFormat};
use crate::config;
use crate::error::OlError;
pub fn run_status(output: &OutputConfig) -> Result<(), OlError> {
let version = env!("OPENLATCH_VERSION");
let cfg = config::Config::load(None, None, false)?;
let pid_opt = lifecycle::read_pid_file();
let is_running = pid_opt.map(lifecycle::is_process_alive).unwrap_or(false);
if is_running {
let pid = pid_opt.unwrap();
show_running_status(version, cfg.port, pid, output, &cfg.cloud.api_url)?;
} else {
show_stopped_status(version, output)?;
}
Ok(())
}
fn show_running_status(
version: &str,
port: u16,
pid: u32,
output: &OutputConfig,
cloud_api_url: &str,
) -> Result<(), OlError> {
let health = fetch_health(port);
let metrics = fetch_metrics(port);
let uptime_str = health
.as_ref()
.and_then(|h| h.get("uptime_secs"))
.and_then(|v| v.as_u64())
.map(format_uptime)
.unwrap_or_else(|| "unknown".to_string());
let agent_str = health
.as_ref()
.and_then(|h| h.get("agent"))
.and_then(|v| v.as_str())
.unwrap_or("Claude Code")
.to_string();
let stale_daemon = stale_daemon_line(
version,
health
.as_ref()
.and_then(|h| h.get("version"))
.and_then(|v| v.as_str()),
);
let update_available = metrics
.as_ref()
.and_then(|m| m.get("update_available"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let total_events = metrics
.as_ref()
.and_then(|m| m.get("events_processed"))
.and_then(|v| v.as_u64())
.unwrap_or(0);
let log_path = config::openlatch_dir().join("logs").join(format!(
"events-{}.jsonl",
chrono::Local::now().format("%Y-%m-%d")
));
let cloud_status_daemon = metrics
.as_ref()
.and_then(|m| m.get("cloud_status"))
.and_then(|v| v.as_str())
.unwrap_or("not_configured")
.to_string();
let cloud_forwarded_count = metrics
.as_ref()
.and_then(|m| m.get("cloud_forwarded_count"))
.and_then(|v| v.as_u64())
.unwrap_or(0);
let cloud_last_sync_secs = metrics
.as_ref()
.and_then(|m| m.get("cloud_last_sync_secs"))
.and_then(|v| v.as_u64())
.unwrap_or(0);
let live_cloud_status = match cloud_status_daemon.as_str() {
"not_configured" | "no_credential" => {
if check_credential_configured() == "not_configured" {
"not_configured".to_string()
} else {
probe_cloud_status(cloud_api_url).to_string()
}
}
_ => cloud_status_daemon.clone(),
};
let cloud_last_sync_relative = format_relative_secs(cloud_last_sync_secs);
let supervision_line = supervision_summary_line();
let install_state = crate::install_state::InstallState::load_or_default();
#[cfg(feature = "boundary")]
let (boundary_port, boundary_state): (Option<u16>, &'static str) = {
let cfg = config::Config::load(None, None, false).ok();
let port = cfg
.as_ref()
.map_or_else(crate::boundary::resolve_boundary_port, |c| c.boundary.port);
let state = match cfg {
Some(cfg) => {
let wired = crate::hooks::detect_agent()
.ok()
.and_then(|agent| match agent {
crate::hooks::DetectedAgent::ClaudeCode { settings_path, .. } => {
crate::cli::commands::boundary::read_boundary_base_url(&settings_path)
}
});
crate::cli::commands::boundary::classify_boundary(&cfg, wired.as_deref()).label()
}
None => "down",
};
(Some(port), state)
};
#[cfg(not(feature = "boundary"))]
let (boundary_port, boundary_state): (Option<u16>, &'static str) = (None, "down");
let degraded_subsystems = degraded_subsystems_from_health(health.as_ref());
if output.format == OutputFormat::Json {
let json = serde_json::json!({
"status": "running",
"version": version,
"port": port,
"pid": pid,
"uptime": uptime_str,
"events": total_events,
"log_path": log_path.to_string_lossy(),
"update_available": update_available,
"cloud_status": live_cloud_status,
"cloud_forwarded_count": cloud_forwarded_count,
"cloud_last_sync": cloud_last_sync_relative,
"cloud_api_url": cloud_api_url,
"supervision": supervision_json(),
"install_state": install_state_json(&install_state),
"daemon_version": health
.as_ref()
.and_then(|h| h.get("version"))
.and_then(|v| v.as_str()),
"daemon_stale": stale_daemon.is_some(),
"boundary": {
"port": boundary_port,
"state": boundary_state,
"up": boundary_state == "up",
},
"health_status": health
.as_ref()
.and_then(|h| h.get("status"))
.and_then(|v| v.as_str())
.unwrap_or("unknown"),
"subsystems": health
.as_ref()
.and_then(|h| h.get("subsystems"))
.cloned()
.unwrap_or(serde_json::Value::Null),
});
output.print_json(&json);
} else if !output.quiet {
crate::cli::header::print(output, &["running", &format!("uptime {uptime_str}")]);
eprintln!(" Port: {port}");
eprintln!(" Agent: {agent_str}");
eprintln!(" Events: {total_events}");
eprintln!(" Log: {}", log_path.display());
if let Some(ref latest) = update_available {
eprintln!(" Update: v{latest} available (run: npx openlatch@latest)");
}
for line in install_state_human_lines(&install_state) {
eprintln!(" {line}");
}
if let Some(ref line) = stale_daemon {
eprintln!(" {line}");
}
if live_cloud_status == "not_configured" {
eprintln!(" Cloud: not configured");
eprintln!(" Run 'openlatch auth login' to enable cloud sync");
} else {
eprintln!(" Cloud: {live_cloud_status} ({cloud_api_url})");
eprintln!(
" Synced: {cloud_forwarded_count} events, last {cloud_last_sync_relative}"
);
}
eprintln!(" Supervision: {supervision_line}");
if let Some(bp) = boundary_port {
let label = if boundary_state == "failed" {
"FAILED (agent wired to a port held by a non-OpenLatch process — credential exposed)"
} else {
boundary_state
};
eprintln!(" Boundary: {label} (port {bp})");
}
for line in °raded_subsystems {
eprintln!(" Subsystem: {line}");
}
}
Ok(())
}
fn degraded_subsystems_from_health(health: Option<&serde_json::Value>) -> Vec<String> {
let Some(subsystems) = health
.and_then(|h| h.get("subsystems"))
.and_then(|v| v.as_object())
else {
return Vec::new();
};
let mut lines: Vec<String> = subsystems
.iter()
.filter(|(_, entry)| {
entry
.get("degraded")
.and_then(|v| v.as_bool())
.unwrap_or(false)
})
.map(|(name, entry)| {
let state = entry
.get("state")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let restarts = entry.get("restarts").and_then(|v| v.as_u64()).unwrap_or(0);
let mut line = format!("{name}: {state} ({restarts} restarts)");
if let Some(err) = entry.get("last_error").and_then(|v| v.as_str()) {
line.push_str(&format!(" — {err}"));
}
line
})
.collect();
lines.sort();
lines
}
fn show_stopped_status(version: &str, output: &OutputConfig) -> Result<(), OlError> {
let (last_seen, last_events) = read_last_session_info();
let cloud_status = check_credential_configured();
let supervision_line = supervision_summary_line();
if output.format == OutputFormat::Json {
let json = serde_json::json!({
"status": "stopped",
"version": version,
"last_seen": last_seen,
"last_session_events": last_events,
"cloud_status": cloud_status,
"supervision": supervision_json(),
});
output.print_json(&json);
} else if !output.quiet {
crate::cli::header::print(output, &["stopped"]);
if let Some(ref ts) = last_seen {
let relative = format_relative_time(ts);
eprintln!(" Last seen: {ts} ({relative} ago)");
} else {
eprintln!(" Last seen: never");
}
eprintln!(" Events: {last_events} in last session");
eprintln!(" Suggestion: Run 'openlatch start' to resume");
if cloud_status == "not_configured" {
eprintln!(" Cloud: not configured");
eprintln!(" Run 'openlatch auth login' to enable cloud sync");
} else {
eprintln!(" Cloud: configured (not running)");
}
eprintln!(" Supervision: {supervision_line}");
}
Ok(())
}
fn supervision_summary_line() -> String {
let Ok(cfg) = config::Config::load(None, None, false) else {
return "unknown".to_string();
};
let observed = probe_supervisor_if_active(&cfg.supervision);
render_supervision_line(&cfg.supervision, observed.as_ref())
}
type ObservedSupervisor = Result<crate::supervision::SupervisorStatus, ()>;
fn probe_supervisor_if_active(
cfg: &crate::supervision::SupervisionConfig,
) -> Option<ObservedSupervisor> {
use crate::supervision::{select_supervisor, SupervisionMode};
if cfg.mode != SupervisionMode::Active {
return None;
}
Some(match select_supervisor() {
Some(sup) => sup.status().map_err(|_| ()),
None => Err(()),
})
}
fn render_supervision_line(
cfg: &crate::supervision::SupervisionConfig,
observed: Option<&ObservedSupervisor>,
) -> String {
use crate::supervision::{SupervisionMode, SupervisorKind};
let backend = match cfg.backend {
SupervisorKind::Launchd => "launchd",
SupervisorKind::Systemd => "systemd",
SupervisorKind::TaskScheduler => "task_scheduler",
SupervisorKind::None => "none",
};
match cfg.mode {
SupervisionMode::Active => match observed {
Some(Ok(s)) if s.installed && s.running && s.unit_current => {
format!("{backend} (active)")
}
Some(Ok(s)) if s.installed && s.running => {
format!("{backend} (active, outdated unit — run 'openlatch supervision install')")
}
Some(Ok(s)) if s.installed => {
format!(
"{backend} (configured active, NOT running — {})",
s.description
)
}
Some(Ok(_)) => format!(
"{backend} (configured active, unit missing — run 'openlatch supervision install')"
),
Some(Err(())) => {
format!("{backend} (configured active, no supervisor available on this OS)")
}
None => format!("{backend} (active)"),
},
SupervisionMode::Deferred => {
let reason = cfg.disabled_reason.clone().unwrap_or_default();
if reason.is_empty() {
format!("{backend} (deferred)")
} else {
format!("{backend} (deferred — {reason})")
}
}
SupervisionMode::Disabled => {
let reason = cfg
.disabled_reason
.clone()
.unwrap_or_else(|| "user_opt_out".to_string());
format!("disabled ({reason})")
}
}
}
fn supervision_json() -> serde_json::Value {
use crate::supervision::{SupervisionMode, SupervisorKind};
let cfg = match config::Config::load(None, None, false) {
Ok(c) => c,
Err(_) => return serde_json::json!(null),
};
let backend = match cfg.supervision.backend {
SupervisorKind::Launchd => "launchd",
SupervisorKind::Systemd => "systemd",
SupervisorKind::TaskScheduler => "task_scheduler",
SupervisorKind::None => "none",
};
let mode = match cfg.supervision.mode {
SupervisionMode::Active => "active",
SupervisionMode::Deferred => "deferred",
SupervisionMode::Disabled => "disabled",
};
let mut out = serde_json::json!({
"mode": mode,
"backend": backend,
"disabled_reason": cfg.supervision.disabled_reason,
});
if let Some(Ok(s)) = probe_supervisor_if_active(&cfg.supervision) {
out["installed"] = serde_json::json!(s.installed);
out["running"] = serde_json::json!(s.running);
out["unit_current"] = serde_json::json!(s.unit_current);
out["description"] = serde_json::json!(s.description);
}
out
}
#[cfg(test)]
mod stale_daemon_tests {
use super::stale_daemon_line;
#[test]
fn matching_versions_say_nothing() {
assert_eq!(stale_daemon_line("0.1.16", Some("0.1.16")), None);
}
#[test]
fn a_daemon_left_behind_by_an_upgrade_is_reported_with_its_remedy() {
let line = stale_daemon_line("0.1.16", Some("0.1.15")).expect("must report");
assert!(line.contains("0.1.15"), "must name what is running: {line}");
assert!(
line.contains("0.1.16"),
"must name what is installed: {line}"
);
assert!(
line.contains("openlatch restart"),
"a warning without the remedy is not actionable: {line}"
);
}
#[test]
fn a_daemon_that_reports_no_version_is_not_called_stale() {
assert_eq!(stale_daemon_line("0.1.16", None), None);
}
}
#[cfg(test)]
mod supervision_line_tests {
use super::*;
use crate::supervision::{
SupervisionConfig, SupervisionMode, SupervisorKind, SupervisorStatus,
};
fn active() -> SupervisionConfig {
SupervisionConfig {
mode: SupervisionMode::Active,
backend: SupervisorKind::Systemd,
disabled_reason: None,
}
}
fn observed(
installed: bool,
running: bool,
unit_current: bool,
desc: &str,
) -> ObservedSupervisor {
Ok(SupervisorStatus {
installed,
running,
unit_current,
description: desc.into(),
})
}
#[test]
fn healthy_renders_unchanged() {
let o = observed(true, true, true, "systemd-user (Restart=always active)");
assert_eq!(
render_supervision_line(&active(), Some(&o)),
"systemd (active)"
);
}
#[test]
fn restart_looping_is_not_reported_as_active() {
let o = observed(
true,
false,
true,
"systemd-user (unit present, state: activating)",
);
let line = render_supervision_line(&active(), Some(&o));
assert!(line.contains("NOT running"), "{line}");
assert!(line.contains("activating"), "{line}");
assert_ne!(line, "systemd (active)");
}
#[test]
fn stale_unit_is_flagged_with_the_remedy() {
let o = observed(true, true, false, "systemd-user (running an outdated unit)");
let line = render_supervision_line(&active(), Some(&o));
assert!(line.contains("outdated unit"), "{line}");
assert!(line.contains("supervision install"), "{line}");
}
#[test]
fn missing_unit_is_flagged_with_the_remedy() {
let o = observed(false, false, false, "not installed");
let line = render_supervision_line(&active(), Some(&o));
assert!(line.contains("unit missing"), "{line}");
assert!(line.contains("supervision install"), "{line}");
}
#[test]
fn active_without_a_supervisor_on_this_os_says_so() {
let line = render_supervision_line(&active(), Some(&Err(())));
assert!(line.contains("no supervisor available"), "{line}");
}
#[test]
fn non_active_modes_are_unchanged_and_never_probe() {
let deferred = SupervisionConfig {
mode: SupervisionMode::Deferred,
backend: SupervisorKind::Systemd,
disabled_reason: Some("headless_install_no_gui".into()),
};
assert_eq!(
render_supervision_line(&deferred, None),
"systemd (deferred — headless_install_no_gui)"
);
assert!(probe_supervisor_if_active(&deferred).is_none());
let disabled = SupervisionConfig {
mode: SupervisionMode::Disabled,
backend: SupervisorKind::None,
disabled_reason: Some("user_opt_out".into()),
};
assert_eq!(
render_supervision_line(&disabled, None),
"disabled (user_opt_out)"
);
assert!(probe_supervisor_if_active(&disabled).is_none());
}
}
fn check_credential_configured() -> &'static str {
let store = crate::core::auth::KeyringCredentialStore::new();
let openlatch_dir = config::openlatch_dir();
let agent_id = config::Config::load(None, None, false)
.ok()
.and_then(|c| c.agent_id)
.unwrap_or_default();
let file_store = crate::core::auth::FileCredentialStore::new(
openlatch_dir.join("credentials.enc"),
agent_id,
);
if crate::core::auth::retrieve_credential(
&store as &dyn crate::core::auth::CredentialStore,
&file_store as &dyn crate::core::auth::CredentialStore,
)
.is_ok()
{
"configured"
} else {
"not_configured"
}
}
fn probe_cloud_status(api_url: &str) -> &'static str {
use secrecy::ExposeSecret;
let store = crate::core::auth::KeyringCredentialStore::new();
let openlatch_dir = config::openlatch_dir();
let agent_id = config::Config::load(None, None, false)
.ok()
.and_then(|c| c.agent_id)
.unwrap_or_default();
let file_store = crate::core::auth::FileCredentialStore::new(
openlatch_dir.join("credentials.enc"),
agent_id,
);
let key = match crate::core::auth::retrieve_credential(
&store as &dyn crate::core::auth::CredentialStore,
&file_store as &dyn crate::core::auth::CredentialStore,
) {
Ok(k) => k,
Err(_) => return "not_configured",
};
let client = match reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(3))
.use_rustls_tls()
.build()
{
Ok(c) => c,
Err(_) => return "disconnected",
};
let base = api_url.trim_end_matches('/');
let url = format!("{base}/api/v1/users/me");
let result = client.get(&url).bearer_auth(key.expose_secret()).send();
match result {
Ok(resp) if resp.status().is_success() => "connected",
Ok(resp)
if resp.status() == reqwest::StatusCode::UNAUTHORIZED
|| resp.status() == reqwest::StatusCode::FORBIDDEN =>
{
"auth_error"
}
_ => "disconnected",
}
}
fn format_relative_secs(epoch_secs: u64) -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let now_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
format_relative_secs_at(epoch_secs, now_secs)
}
fn format_relative_secs_at(epoch_secs: u64, now_secs: u64) -> String {
if epoch_secs == 0 {
return "never".to_string();
}
let elapsed = now_secs.saturating_sub(epoch_secs);
if elapsed < 60 {
format!("{elapsed}s ago")
} else if elapsed < 3600 {
format!("{}m ago", elapsed / 60)
} else if elapsed < 86400 {
format!("{}h ago", elapsed / 3600)
} else {
format!("{}d ago", elapsed / 86400)
}
}
fn fetch_health(port: u16) -> Option<serde_json::Value> {
let url = format!("http://127.0.0.1:{port}/health");
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(2))
.use_rustls_tls()
.build()
.ok()?;
let resp = client.get(&url).send().ok()?;
if resp.status().is_success() {
resp.json().ok()
} else {
None
}
}
fn fetch_metrics(port: u16) -> Option<serde_json::Value> {
let url = format!("http://127.0.0.1:{port}/metrics");
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(2))
.use_rustls_tls()
.build()
.ok()?;
let resp = client.get(&url).send().ok()?;
if resp.status().is_success() {
resp.json().ok()
} else {
None
}
}
fn read_last_session_info() -> (Option<String>, u64) {
let log_path = config::openlatch_dir().join("daemon.log");
let Ok(content) = std::fs::read_to_string(&log_path) else {
return (None, 0);
};
let mut last_seen = None;
let mut last_events = 0u64;
for line in content.lines().rev() {
if let Ok(entry) = serde_json::from_str::<serde_json::Value>(line) {
if entry
.get("message")
.and_then(|m| m.as_str())
.map(|m| m.contains("daemon stopped") || m.contains("shutdown"))
.unwrap_or(false)
{
last_seen = entry
.get("timestamp")
.and_then(|t| t.as_str())
.map(|s| s.to_string());
last_events = entry.get("events").and_then(|e| e.as_u64()).unwrap_or(0);
break;
}
}
}
(last_seen, last_events)
}
fn format_uptime(secs: u64) -> String {
if secs < 60 {
format!("{secs}s")
} else if secs < 3600 {
format!("{}m {}s", secs / 60, secs % 60)
} else {
format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
}
}
fn format_relative_time(ts: &str) -> String {
if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(ts) {
let now = chrono::Utc::now();
let duration = now.signed_duration_since(dt.with_timezone(&chrono::Utc));
let secs = duration.num_seconds();
if secs < 60 {
return format!("{secs} seconds");
} else if secs < 3600 {
return format!("{} minutes", secs / 60);
} else if secs < 86400 {
return format!("{} hours", secs / 3600);
} else {
return format!("{} days", secs / 86400);
}
}
"unknown time".to_string()
}
fn install_state_json(state: &crate::install_state::InstallState) -> serde_json::Value {
let drift_detected = matches!(
(
&state.actual_binary_version,
&state.npm_reported_version,
state.install_method,
),
(Some(actual), Some(npm), crate::install_state::InstallMethod::Npm) if actual != npm
);
serde_json::json!({
"install_method": serde_json::to_value(state.install_method).unwrap_or(serde_json::Value::Null),
"actual_binary_version": state.actual_binary_version,
"npm_reported_version": state.npm_reported_version,
"last_updated_at": state.last_updated_at,
"last_check_at": state.last_check_at,
"drift_detected": drift_detected,
})
}
fn stale_daemon_line(cli_version: &str, daemon_version: Option<&str>) -> Option<String> {
let running = daemon_version?;
if running == cli_version {
return None;
}
Some(format!(
"Daemon: serving {running} but {cli_version} is installed — \
the update is not in effect (run: openlatch restart)"
))
}
fn install_state_human_lines(state: &crate::install_state::InstallState) -> Vec<String> {
use crate::install_state::InstallMethod;
let mut lines = Vec::new();
if let Some(actual) = state.actual_binary_version.as_deref() {
let suffix = match state.last_updated_at.as_deref() {
Some(ts) => format!(" (last updated {ts})"),
None => String::new(),
};
lines.push(format!("Binary: {actual}{suffix}"));
}
if matches!(state.install_method, InstallMethod::Npm) {
match (
state.npm_reported_version.as_deref(),
state.actual_binary_version.as_deref(),
) {
(Some(npm), Some(actual)) if npm != actual => {
lines.push(format!(
"npm: {npm} ⚠ drift — run `npm install -g @openlatch/client@{actual}` to sync"
));
}
(Some(npm), _) => {
lines.push(format!("npm: {npm}"));
}
_ => {}
}
}
lines
}
#[cfg(test)]
mod tests {
use super::*;
const NOW: u64 = 1_767_225_600;
#[test]
fn test_format_relative_secs_zero_returns_never() {
assert_eq!(format_relative_secs(0), "never");
assert_eq!(format_relative_secs_at(0, NOW), "never");
}
#[test]
fn degraded_subsystems_reports_only_flagged_entries() {
let health = serde_json::json!({
"status": "degraded",
"subsystems": {
"cloud-worker": {"state": "running", "restarts": 0},
"update-check": {"state": "stopped", "restarts": 0},
"boundary": {
"state": "restarting",
"restarts": 3,
"degraded": true,
"last_error": "boundary listener could not bind pinned loopback port 7600",
},
},
});
let lines = degraded_subsystems_from_health(Some(&health));
assert_eq!(lines.len(), 1, "got {lines:?}");
assert!(
lines[0].starts_with("boundary: restarting (3 restarts)"),
"{}",
lines[0]
);
assert!(lines[0].contains("could not bind"), "{}", lines[0]);
}
#[test]
fn degraded_subsystems_is_empty_when_healthy_or_absent() {
let healthy = serde_json::json!({
"status": "ok",
"subsystems": {"cloud-worker": {"state": "running", "restarts": 0}},
});
assert!(degraded_subsystems_from_health(Some(&healthy)).is_empty());
let legacy = serde_json::json!({"status": "ok", "version": "0.1.0"});
assert!(degraded_subsystems_from_health(Some(&legacy)).is_empty());
assert!(degraded_subsystems_from_health(None).is_empty());
}
#[cfg(feature = "boundary")]
#[test]
fn boundary_state_maps_onto_dashboard_vocabulary() {
use crate::cli::commands::boundary::BoundaryState;
assert_eq!(BoundaryState::Wired.label(), "up");
assert_eq!(BoundaryState::WiredToForeign.label(), "failed");
assert_eq!(BoundaryState::Disabled.label(), "disabled");
assert_eq!(BoundaryState::ForeignIdle.label(), "down");
assert_eq!(BoundaryState::Down.label(), "down");
assert_ne!(
BoundaryState::ForeignIdle.label(),
"failed",
"a foreign process on an unwired port is not an exposure"
);
}
#[test]
fn test_format_relative_secs_12_returns_12s_ago() {
assert_eq!(format_relative_secs_at(NOW - 12, NOW), "12s ago");
}
#[test]
fn test_format_relative_secs_90_returns_1m_ago() {
assert_eq!(format_relative_secs_at(NOW - 90, NOW), "1m ago");
}
#[test]
fn test_format_relative_secs_3700_returns_1h_ago() {
assert_eq!(format_relative_secs_at(NOW - 3700, NOW), "1h ago");
}
}