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 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 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 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 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 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 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 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}