oxur_cli/repl/
connect.rs

1//! Connect mode REPL client implementation
2//!
3//! Provides REPL client that connects to a remote server via TCP.
4
5use crate::config::ReplConfig;
6use crate::repl::runner::{ReplClientAdapter, ReplRunner};
7use crate::repl::stats::{
8    show_cache_from_snapshot, show_execution_from_snapshot, show_server_stats,
9    show_session_summary_from_snapshot, show_subprocess_stats,
10};
11use crate::repl::terminal::ReplTerminal;
12use anyhow::{Context, Result};
13use async_trait::async_trait;
14use oxur_repl::protocol::{
15    MessageId, Operation, OperationResult, ReplMode, Request, Response, SessionId,
16};
17use oxur_repl::transport::{TcpTransport, Transport};
18use std::sync::atomic::{AtomicU64, Ordering};
19
20/// TCP client adapter for connect mode
21///
22/// Wraps TcpTransport and provides stats command handling via protocol operations.
23struct TcpAdapter {
24    transport: TcpTransport,
25    session_id: SessionId,
26    msg_counter: AtomicU64,
27}
28
29impl TcpAdapter {
30    fn new(transport: TcpTransport, session_id: SessionId) -> Self {
31        Self { transport, session_id, msg_counter: AtomicU64::new(1000) }
32    }
33
34    /// Get the next message ID for internal protocol requests
35    fn next_message_id(&self) -> MessageId {
36        MessageId::new(self.msg_counter.fetch_add(1, Ordering::SeqCst))
37    }
38
39    /// Send a stats request and receive the response
40    async fn request_stats(&mut self, operation: Operation) -> Result<OperationResult> {
41        let request =
42            Request { id: self.next_message_id(), session_id: self.session_id.clone(), operation };
43        self.transport.send_request(&request).await?;
44        let response = self.transport.recv_response().await?;
45        Ok(response.result)
46    }
47}
48
49#[async_trait]
50impl ReplClientAdapter for TcpAdapter {
51    async fn send_eval(&mut self, request: Request) -> Result<()> {
52        self.transport.send_request(&request).await.context("Failed to send request")
53    }
54
55    async fn recv_response(&mut self) -> Result<Response> {
56        self.transport.recv_response().await.context("Failed to receive response")
57    }
58
59    async fn close(&mut self) -> Result<()> {
60        self.transport.close().await.context("Failed to close connection")
61    }
62
63    fn current_session(&self) -> &SessionId {
64        &self.session_id
65    }
66
67    async fn handle_special_command(&mut self, input: &str, color_enabled: bool) -> Option<String> {
68        // Handle stats commands via protocol
69        if !input.starts_with("(stats") {
70            return None;
71        }
72
73        match input {
74            "(stats server)" => match self.request_stats(Operation::GetServerStats).await {
75                Ok(OperationResult::ServerStats { snapshot }) => {
76                    Some(show_server_stats(&snapshot, color_enabled))
77                }
78                Ok(_) => Some("Unexpected response from server".to_string()),
79                Err(e) => Some(format!("Failed to get server stats: {}", e)),
80            },
81            "(stats subprocess)" => match self.request_stats(Operation::GetSubprocessStats).await {
82                Ok(OperationResult::SubprocessStats { snapshot }) => {
83                    Some(show_subprocess_stats(&snapshot, color_enabled))
84                }
85                Ok(_) => Some("Unexpected response from server".to_string()),
86                Err(e) => Some(format!("Failed to get subprocess stats: {}", e)),
87            },
88            "(stats)" => match self.request_stats(Operation::GetSessionStats).await {
89                Ok(OperationResult::SessionStats { snapshot }) => {
90                    Some(show_session_summary_from_snapshot(&snapshot, color_enabled))
91                }
92                Ok(_) => Some("Unexpected response from server".to_string()),
93                Err(e) => Some(format!("Failed to get session stats: {}", e)),
94            },
95            "(stats execution)" => match self.request_stats(Operation::GetSessionStats).await {
96                Ok(OperationResult::SessionStats { snapshot }) => {
97                    Some(show_execution_from_snapshot(&snapshot, color_enabled))
98                }
99                Ok(_) => Some("Unexpected response from server".to_string()),
100                Err(e) => Some(format!("Failed to get session stats: {}", e)),
101            },
102            "(stats cache)" => match self.request_stats(Operation::GetSessionStats).await {
103                Ok(OperationResult::SessionStats { snapshot }) => {
104                    Some(show_cache_from_snapshot(&snapshot, color_enabled))
105                }
106                Ok(_) => Some("Unexpected response from server".to_string()),
107                Err(e) => Some(format!("Failed to get session stats: {}", e)),
108            },
109            "(stats resources)" => {
110                // Resources require local filesystem access, not available remotely
111                Some("Resource stats not available in remote mode (requires local filesystem access)".to_string())
112            }
113            _ => {
114                // Unknown stats subcommand
115                Some(format!(
116                    "Unknown stats command: {}. Try (help stats) for available commands.",
117                    input
118                ))
119            }
120        }
121    }
122}
123
124/// Run the connect mode REPL
125///
126/// Connects to an existing REPL server and provides terminal interface
127/// for sending commands and receiving results.
128pub async fn run(addr: &str, config: ReplConfig) -> Result<()> {
129    // Capture system metadata at startup
130    let system_metadata = oxur_repl::metadata::SystemMetadata::capture();
131
132    // Connect to server
133    let transport = TcpTransport::connect(addr)
134        .await
135        .context(format!("Failed to connect to REPL server at {}", addr))?;
136
137    // Generate unique session ID
138    let session_id = SessionId::new(format!("connect-{}", std::process::id()));
139
140    // Create adapter with session_id for stats protocol support
141    let mut adapter = TcpAdapter::new(transport, session_id.clone());
142
143    // Create session on server
144    let create_req = Request {
145        id: adapter.next_message_id(),
146        session_id: session_id.clone(),
147        operation: Operation::CreateSession { mode: ReplMode::Lisp },
148    };
149
150    adapter.send_eval(create_req).await.context("Failed to send create session request")?;
151    let _response = adapter.recv_response().await.context("Failed to receive create response")?;
152
153    // Create terminal interface with configuration
154    let terminal = ReplTerminal::with_config(config.terminal, config.history)
155        .context("Failed to create terminal")?;
156
157    // Create runner
158    let mut runner = ReplRunner::new(terminal, session_id);
159
160    // Print welcome banner and connection info
161    runner.print_banner(&system_metadata);
162    print_connection_info(addr, runner.terminal().config().color_enabled);
163
164    // Run the REPL loop
165    runner.run(&mut adapter).await?;
166    runner.finish(&mut adapter).await?;
167
168    Ok(())
169}
170
171/// Print connection info message
172fn print_connection_info(addr: &str, color_enabled: bool) {
173    if color_enabled {
174        println!("\x1b[36mConnected to {}\x1b[0m\n", addr);
175    } else {
176        println!("Connected to {}\n", addr);
177    }
178}