Skip to main content

vibekit_proxy/
cli.rs

1use clap::{Arg, Command};
2use tokio::process::Command as TokioCommand;
3use tracing::error;
4
5use crate::{ProxyServer, Result};
6
7pub async fn run_cli() -> Result<()> {
8    let matches = Command::new("vibekit-proxy")
9        .version("0.0.21")
10        .about("VibeKit proxy server for secure API routing")
11        .arg(
12            Arg::new("port")
13                .short('p')
14                .long("port")
15                .value_name("PORT")
16                .help("Port to run on")
17                .default_value("8080"),
18        )
19        .arg(
20            Arg::new("config")
21                .short('c')
22                .long("config")
23                .value_name("PATH")
24                .help("Path to config.yaml file")
25                .default_value("config.yaml"),
26        )
27        .subcommand(
28            Command::new("start")
29                .about("Start the proxy server")
30                .arg(
31                    Arg::new("port")
32                        .short('p')
33                        .long("port")
34                        .value_name("PORT")
35                        .help("Port to run on")
36                        .default_value("8080"),
37                )
38                .arg(
39                    Arg::new("config")
40                        .short('c')
41                        .long("config")
42                        .value_name("PATH")
43                        .help("Path to config.yaml file")
44                        .default_value("config.yaml"),
45                )
46                .arg(
47                    Arg::new("daemon")
48                        .short('d')
49                        .long("daemon")
50                        .help("Run in background")
51                        .action(clap::ArgAction::SetTrue),
52                ),
53        )
54        .subcommand(
55            Command::new("stop")
56                .about("Stop the proxy server")
57                .arg(
58                    Arg::new("port")
59                        .short('p')
60                        .long("port")
61                        .value_name("PORT")
62                        .help("Port to stop")
63                        .default_value("8080"),
64                ),
65        )
66        .subcommand(
67            Command::new("status")
68                .about("Show proxy server status")
69                .arg(
70                    Arg::new("port")
71                        .short('p')
72                        .long("port")
73                        .value_name("PORT")
74                        .help("Port to check")
75                        .default_value("8080"),
76                ),
77        )
78        .get_matches();
79
80    match matches.subcommand() {
81        Some(("start", sub_matches)) => {
82            let port: u16 = sub_matches
83                .get_one::<String>("port")
84                .unwrap()
85                .parse()
86                .unwrap_or_else(|_| {
87                    // Check environment variable
88                    std::env::var("PORT")
89                        .ok()
90                        .and_then(|p| p.parse().ok())
91                        .unwrap_or(8080)
92                });
93
94            let config_path = sub_matches
95                .get_one::<String>("config")
96                .unwrap()
97                .clone();
98
99            let _daemon = sub_matches.get_flag("daemon");
100
101            // Daemon mode (simplified)
102
103            start_proxy_with_check(port, Some(config_path)).await?;
104        }
105        Some(("stop", sub_matches)) => {
106            let port: u16 = sub_matches
107                .get_one::<String>("port")
108                .unwrap()
109                .parse()
110                .unwrap_or(8080);
111
112            stop_proxy_on_port(port).await;
113        }
114        Some(("status", sub_matches)) => {
115            let port: u16 = sub_matches
116                .get_one::<String>("port")
117                .unwrap()
118                .parse()
119                .unwrap_or(8080);
120
121            show_proxy_status(port).await;
122        }
123        _ => {
124            // Default command - start the proxy server
125            let port: u16 = matches
126                .get_one::<String>("port")
127                .unwrap()
128                .parse()
129                .unwrap_or(8080);
130
131            let config_path = matches
132                .get_one::<String>("config")
133                .unwrap()
134                .clone();
135
136            start_proxy_with_check(port, Some(config_path)).await?;
137        }
138    }
139
140    Ok(())
141}
142
143async fn start_proxy_with_check(port: u16, config_path: Option<String>) -> Result<()> {
144    use std::sync::Arc;
145    use tokio::signal;
146    
147    let server = Arc::new(ProxyServer::new(port, config_path).await?);
148    let server_clone = Arc::clone(&server);
149
150    // Handle graceful shutdown
151    tokio::spawn(async move {
152        let _ = signal::ctrl_c().await;
153        server_clone.stop().await;
154    });
155
156    server.start().await
157}
158
159async fn stop_proxy_on_port(port: u16) {
160    // Kill processes using the port (Unix/Linux/macOS only)
161    if let Ok(output) = TokioCommand::new("lsof")
162        .arg("-ti")
163        .arg(format!(":{}", port))
164        .output()
165        .await
166    {
167        let pids_str = String::from_utf8_lossy(&output.stdout);
168        let pids: Vec<&str> = pids_str.trim().split('\n').filter(|s| !s.is_empty()).collect();
169
170        if pids.is_empty() {
171            return;
172        }
173
174        for pid_str in &pids {
175            if let Ok(pid) = pid_str.parse::<i32>() {
176                let _ = TokioCommand::new("kill")
177                    .arg("-TERM")
178                    .arg(pid.to_string())
179                    .output()
180                    .await;
181            }
182        }
183    } else {
184        error!("Failed to find processes on port {}", port);
185    }
186}
187
188async fn show_proxy_status(port: u16) {
189    println!("🌐 Proxy Server Status");
190    println!("{}", "─".repeat(30));
191
192    let running = is_port_in_use(port).await;
193    println!("Port {}: {}", port, if running { "✅ RUNNING" } else { "❌ NOT RUNNING" });
194
195    if running {
196        // Try to get health check
197        let health_url = format!("http://localhost:{}/health", port);
198        match reqwest::get(&health_url).await {
199            Ok(response) => {
200                if response.status().is_success() {
201                    if let Ok(data) = response.json::<serde_json::Value>().await {
202                        if let Some(uptime) = data.get("uptime").and_then(|u| u.as_u64()) {
203                            println!("Uptime: {}s", uptime);
204                        }
205                        if let Some(request_count) = data.get("requestCount").and_then(|r| r.as_u64()) {
206                            println!("Requests: {}", request_count);
207                        }
208                    }
209                } else {
210                    println!("Health check: ❌ Failed");
211                }
212            }
213            Err(_) => {
214                println!("Health check: ❌ Failed");
215            }
216        }
217
218        // Show process info
219        if let Ok(output) = TokioCommand::new("lsof")
220            .arg("-ti")
221            .arg(format!(":{}", port))
222            .output()
223            .await
224        {
225            let pids_str = String::from_utf8_lossy(&output.stdout);
226            let pids: Vec<&str> = pids_str.trim().split('\n').filter(|s| !s.is_empty()).collect();
227            if !pids.is_empty() {
228                println!("PIDs: {}", pids.join(", "));
229            }
230        }
231    }
232}
233
234async fn is_port_in_use(port: u16) -> bool {
235    let output = TokioCommand::new("lsof")
236        .arg("-ti")
237        .arg(format!(":{}", port))
238        .output()
239        .await;
240
241    if let Ok(output) = output {
242        !String::from_utf8_lossy(&output.stdout).trim().is_empty()
243    } else {
244        false
245    }
246}