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!("CARGO_PKG_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)?;
} else {
show_stopped_status(version, output)?;
}
Ok(())
}
fn show_running_status(
version: &str,
port: u16,
pid: u32,
output: &OutputConfig,
) -> 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 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")
));
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,
});
output.print_json(&json);
} else if !output.quiet {
eprintln!("openlatch v{version} running 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)");
}
}
Ok(())
}
fn show_stopped_status(version: &str, output: &OutputConfig) -> Result<(), OlError> {
let (last_seen, last_events) = read_last_session_info();
if output.format == OutputFormat::Json {
let json = serde_json::json!({
"status": "stopped",
"version": version,
"last_seen": last_seen,
"last_session_events": last_events,
});
output.print_json(&json);
} else if !output.quiet {
eprintln!("openlatch v{version} 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");
}
Ok(())
}
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))
.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))
.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()
}