Skip to main content

life_cli/
relay.rs

1//! `life relay` — manage the relay daemon for remote agent sessions.
2//!
3//! Wraps life-relayd as a library so users run `life relay auth|start|stop|status`
4//! instead of a separate binary. Shares credentials with the broomva CLI
5//! (`~/.broomva/config.json`) so `broomva auth login` tokens work here too.
6
7use anyhow::Result;
8use clap::Subcommand;
9use tracing::info;
10
11#[derive(Subcommand)]
12pub enum RelayCommand {
13    /// Authenticate with broomva.tech via device authorization.
14    /// If already logged in via `broomva auth login`, those credentials are reused.
15    Auth {
16        /// Server URL.
17        #[arg(long, default_value = "https://broomva.tech")]
18        url: String,
19    },
20    /// Start the relay daemon (connects to broomva.tech, polls for commands).
21    Start {
22        /// Local API bind address.
23        #[arg(long, default_value = "127.0.0.1:3004")]
24        bind: String,
25        /// Server URL to connect to.
26        #[arg(long, default_value = "https://broomva.tech")]
27        server: String,
28    },
29    /// Stop the relay daemon.
30    Stop,
31    /// Show relay daemon and connection status.
32    Status,
33}
34
35pub async fn run(command: RelayCommand) -> Result<()> {
36    match command {
37        RelayCommand::Auth { url } => {
38            let cfg = life_relayd::config::load_config()?;
39
40            // Check if broomva CLI token already exists
41            match life_relayd::config::read_token(&cfg) {
42                Ok(_) => {
43                    println!("  Already authenticated (token found).");
44                    println!("  Source: broomva CLI or relay credentials.");
45                    println!();
46                    println!("  To re-authenticate, run `life relay auth --url {url}`");
47                    println!("  with a fresh device code flow.");
48                }
49                Err(_) => {
50                    info!(url = %url, "starting device authorization");
51                    life_relayd::auth::run(&url, &cfg.credentials_path()).await?;
52                }
53            }
54        }
55        RelayCommand::Start { bind, server } => {
56            info!(bind = %bind, server = %server, "starting relay daemon");
57            life_relayd::daemon::run(&bind, &server).await?;
58        }
59        RelayCommand::Stop => {
60            // Send stop signal to running daemon via local API
61            let client = reqwest::Client::new();
62            match client
63                .get("http://127.0.0.1:3004/health")
64                .timeout(std::time::Duration::from_secs(2))
65                .send()
66                .await
67            {
68                Ok(_) => {
69                    println!("  Relay daemon is running but graceful stop is not yet implemented.");
70                    println!("  Use `pkill -f life-relayd` or `kill $(lsof -ti :3004)` to stop.");
71                }
72                Err(_) => {
73                    println!("  No relay daemon running on port 3004.");
74                }
75            }
76        }
77        RelayCommand::Status => {
78            let cfg = life_relayd::config::load_config()?;
79            let has_token = life_relayd::config::read_token(&cfg).is_ok();
80
81            println!("  Relay Configuration");
82            println!("  ───────────────────");
83            println!("  Config dir:     {}", cfg.config_dir.display());
84            println!("  Authenticated:  {}", if has_token { "yes" } else { "no" });
85
86            // Check if daemon is running
87            let client = reqwest::Client::new();
88            match client
89                .get("http://127.0.0.1:3004/health")
90                .timeout(std::time::Duration::from_secs(2))
91                .send()
92                .await
93            {
94                Ok(resp) if resp.status().is_success() => {
95                    println!("  Daemon:         running (port 3004)");
96                    if let Ok(body) = resp.json::<serde_json::Value>().await {
97                        if let Some(v) = body.get("version").and_then(|v| v.as_str()) {
98                            println!("  Version:        {v}");
99                        }
100                    }
101                }
102                _ => {
103                    println!("  Daemon:         not running");
104                }
105            }
106
107            if !has_token {
108                println!();
109                println!("  Run `life relay auth` or `broomva auth login` to authenticate.");
110            }
111        }
112    }
113
114    Ok(())
115}