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("ai-proxy")
9 .version(env!("CARGO_PKG_VERSION"))
10 .about("AI 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 vibekit.yaml file")
25 .default_value("vibekit.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 vibekit.yaml file")
44 .default_value("vibekit.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 .arg(
54 Arg::new("redaction_api_url")
55 .long("redaction-api-url")
56 .value_name("URL")
57 .help("URL for redaction API to screen user messages")
58 .required(false),
59 ),
60 )
61 .subcommand(
62 Command::new("stop")
63 .about("Stop the proxy server")
64 .arg(
65 Arg::new("port")
66 .short('p')
67 .long("port")
68 .value_name("PORT")
69 .help("Port to stop")
70 .default_value("8080"),
71 ),
72 )
73 .subcommand(
74 Command::new("status")
75 .about("Show proxy server status")
76 .arg(
77 Arg::new("port")
78 .short('p')
79 .long("port")
80 .value_name("PORT")
81 .help("Port to check")
82 .default_value("8080"),
83 ),
84 )
85 .get_matches();
86
87 match matches.subcommand() {
88 Some(("start", sub_matches)) => {
89 let port: u16 = sub_matches
90 .get_one::<String>("port")
91 .unwrap()
92 .parse()
93 .unwrap_or_else(|_| {
94 std::env::var("PORT")
96 .ok()
97 .and_then(|p| p.parse().ok())
98 .unwrap_or(8080)
99 });
100
101 let config_path = sub_matches
102 .get_one::<String>("config")
103 .unwrap()
104 .clone();
105
106 let redaction_api_url = sub_matches
107 .get_one::<String>("redaction_api_url")
108 .cloned()
109 .or_else(|| std::env::var("VIBEKIT_REDACTION_API_URL").ok());
110
111 let _daemon = sub_matches.get_flag("daemon");
112
113 start_proxy_with_check(port, Some(config_path), redaction_api_url).await?;
116 }
117 Some(("stop", sub_matches)) => {
118 let port: u16 = sub_matches
119 .get_one::<String>("port")
120 .unwrap()
121 .parse()
122 .unwrap_or(8080);
123
124 stop_proxy_on_port(port).await;
125 }
126 Some(("status", sub_matches)) => {
127 let port: u16 = sub_matches
128 .get_one::<String>("port")
129 .unwrap()
130 .parse()
131 .unwrap_or(8080);
132
133 show_proxy_status(port).await;
134 }
135 _ => {
136 let port: u16 = matches
138 .get_one::<String>("port")
139 .unwrap()
140 .parse()
141 .unwrap_or(8080);
142
143 let config_path = matches
144 .get_one::<String>("config")
145 .unwrap()
146 .clone();
147
148 let redaction_api_url = matches
149 .get_one::<String>("redaction_api_url")
150 .cloned()
151 .or_else(|| std::env::var("VIBEKIT_REDACTION_API_URL").ok());
152
153 start_proxy_with_check(port, Some(config_path), redaction_api_url).await?;
154 }
155 }
156
157 Ok(())
158}
159
160async fn start_proxy_with_check(port: u16, config_path: Option<String>, redaction_api_url: Option<String>) -> Result<()> {
161 use std::sync::Arc;
162 use tokio::signal;
163
164 let server = Arc::new(ProxyServer::new(port, config_path, redaction_api_url).await?);
165 let server_clone = Arc::clone(&server);
166
167 tokio::spawn(async move {
169 let _ = signal::ctrl_c().await;
170 server_clone.stop().await;
171 });
172
173 server.start().await
174}
175
176async fn stop_proxy_on_port(port: u16) {
177 if let Ok(output) = TokioCommand::new("lsof")
179 .arg("-ti")
180 .arg(format!(":{}", port))
181 .output()
182 .await
183 {
184 let pids_str = String::from_utf8_lossy(&output.stdout);
185 let pids: Vec<&str> = pids_str.trim().split('\n').filter(|s| !s.is_empty()).collect();
186
187 if pids.is_empty() {
188 return;
189 }
190
191 for pid_str in &pids {
192 if let Ok(pid) = pid_str.parse::<i32>() {
193 let _ = TokioCommand::new("kill")
194 .arg("-TERM")
195 .arg(pid.to_string())
196 .output()
197 .await;
198 }
199 }
200 } else {
201 error!("Failed to find processes on port {}", port);
202 }
203}
204
205async fn show_proxy_status(port: u16) {
206 println!("🌐 Proxy Server Status");
207 println!("{}", "─".repeat(30));
208
209 let running = is_port_in_use(port).await;
210 println!("Port {}: {}", port, if running { "✅ RUNNING" } else { "❌ NOT RUNNING" });
211
212 if running {
213 let health_url = format!("http://localhost:{}/health", port);
215 match reqwest::get(&health_url).await {
216 Ok(response) => {
217 if response.status().is_success() {
218 if let Ok(data) = response.json::<serde_json::Value>().await {
219 if let Some(uptime) = data.get("uptime").and_then(|u| u.as_u64()) {
220 println!("Uptime: {}s", uptime);
221 }
222 if let Some(request_count) = data.get("requestCount").and_then(|r| r.as_u64()) {
223 println!("Requests: {}", request_count);
224 }
225 }
226 } else {
227 println!("Health check: ❌ Failed");
228 }
229 }
230 Err(_) => {
231 println!("Health check: ❌ Failed");
232 }
233 }
234
235 if let Ok(output) = TokioCommand::new("lsof")
237 .arg("-ti")
238 .arg(format!(":{}", port))
239 .output()
240 .await
241 {
242 let pids_str = String::from_utf8_lossy(&output.stdout);
243 let pids: Vec<&str> = pids_str.trim().split('\n').filter(|s| !s.is_empty()).collect();
244 if !pids.is_empty() {
245 println!("PIDs: {}", pids.join(", "));
246 }
247 }
248 }
249}
250
251async fn is_port_in_use(port: u16) -> bool {
252 let output = TokioCommand::new("lsof")
253 .arg("-ti")
254 .arg(format!(":{}", port))
255 .output()
256 .await;
257
258 if let Ok(output) = output {
259 !String::from_utf8_lossy(&output.stdout).trim().is_empty()
260 } else {
261 false
262 }
263}