vex-cli 0.7.13

AI-native version control workflow engine
use serde_json::{Value, json};
use tungstenite::{Message, connect};

pub struct DaemonClient {
    socket: tungstenite::WebSocket<tungstenite::stream::MaybeTlsStream<std::net::TcpStream>>,
    request_counter: u64,
}

impl DaemonClient {
    /// Connect to a WebSocket URL and authenticate.
    pub fn connect(url: &str, token: &str) -> Result<Self, String> {
        let (mut socket, _) = connect(url).map_err(|e| format!("failed to connect: {e}"))?;

        // Send auth
        let auth_msg = json!({"type": "auth", "token": token});
        socket
            .send(Message::Text(auth_msg.to_string().into()))
            .map_err(|e| format!("auth send failed: {e}"))?;

        // Read auth response
        let response = socket
            .read()
            .map_err(|e| format!("auth response failed: {e}"))?;
        if let Message::Text(text) = response {
            let val: Value =
                serde_json::from_str(&text).map_err(|e| format!("invalid auth response: {e}"))?;
            if val.get("status").and_then(|v| v.as_str()) != Some("ok") {
                let msg = val
                    .get("message")
                    .and_then(|v| v.as_str())
                    .unwrap_or("unknown error");
                return Err(format!("auth failed: {msg}"));
            }
        } else {
            return Err("unexpected auth response type".into());
        }

        Ok(Self {
            socket,
            request_counter: 0,
        })
    }

    /// Send a request and wait for the response.
    pub fn request(&mut self, method: &str, params: Value) -> Result<Value, String> {
        self.request_counter += 1;
        let id = format!("cli-{}", self.request_counter);

        let msg = json!({
            "type": "request",
            "id": id,
            "method": method,
            "params": params,
        });

        self.socket
            .send(Message::Text(msg.to_string().into()))
            .map_err(|e| format!("send failed: {e}"))?;

        // Set a 30-second read timeout to avoid blocking forever
        if let tungstenite::stream::MaybeTlsStream::Plain(ref s) = *self.socket.get_ref() {
            let _ = s.set_read_timeout(Some(std::time::Duration::from_secs(30)));
        }

        // Read responses until we get ours (skip events)
        loop {
            let response = self
                .socket
                .read()
                .map_err(|e| format!("read failed: {e}"))?;
            if let Message::Text(text) = response {
                let val: Value =
                    serde_json::from_str(&text).map_err(|e| format!("invalid response: {e}"))?;
                // Check if this is our response
                if val.get("id").and_then(|v| v.as_str()) == Some(&id) {
                    if let Some(error) = val.get("error").and_then(|v| v.as_str()) {
                        return Err(error.to_string());
                    }
                    return Ok(val.get("result").cloned().unwrap_or(Value::Null));
                }
                // Otherwise it's an event or someone else's response — skip
            }
        }
    }
}

/// Connect to the local hub or exit with an error.
pub fn connect_to_hub_or_exit() -> DaemonClient {
    let vex_home = match vex_app::VexHome::new(None) {
        Ok(h) => h,
        Err(e) => {
            eprintln!("Error: {e}");
            std::process::exit(1);
        }
    };

    let hub_config = vex_home.load_hub_config().unwrap_or_default();
    let port = hub_config.web_port;
    let url = format!("ws://127.0.0.1:{port}/ws");

    let token = match vex_home.load_or_create_token() {
        Ok(t) => t,
        Err(e) => {
            eprintln!("Error reading token: {e}");
            std::process::exit(1);
        }
    };

    match DaemonClient::connect(&url, &token) {
        Ok(c) => c,
        Err(e) => {
            eprintln!("Error: {e}");
            eprintln!("Is the hub running? Start it with: vex hub start");
            std::process::exit(1);
        }
    }
}