use std::net::{TcpStream, ToSocketAddrs};
use std::time::Duration;
use anyhow::{Result, bail};
use crate::apps::{self, Apps, Env};
use crate::project;
fn load() -> Result<Apps> {
let root = project::find_root()?;
let env = Env::load(&root)?;
let mcp = mcp_upstream(&env);
match Apps::load(&root, &env, &mcp)? {
Some(apps) => Ok(apps),
None => bail!("{} not found in this project", apps::CONFIG),
}
}
pub fn mcp_upstream(env: &Env) -> String {
let host = env.get("APP_HOST").unwrap_or_else(|| "127.0.0.1".into());
let port = env
.get("APP_PORT")
.and_then(|port| port.trim().parse().ok())
.unwrap_or(3000);
apps::connect_address(&host, port)
}
pub fn list() -> Result<()> {
let apps = load()?;
let rows: Vec<[String; 4]> = apps
.routes
.iter()
.map(|route| {
let app = if route.is_mcp {
"mcp (backend)".to_owned()
} else {
route.app.clone()
};
let status = if is_up(&route.upstream) { "up" } else { "down" };
[
route.host.clone(),
app,
route.upstream.clone(),
status.to_owned(),
]
})
.collect();
let header = ["HOST", "APP", "UPSTREAM", "STATUS"];
let widths: Vec<usize> = (0..4)
.map(|i| {
rows.iter()
.map(|row| row[i].len())
.chain([header[i].len()])
.max()
.unwrap_or(0)
})
.collect();
let print = |row: &[&str]| {
let line: Vec<String> = row
.iter()
.zip(&widths)
.map(|(cell, width)| format!("{cell:<width$}"))
.collect();
println!(" {}", line.join(" ").trim_end());
};
println!();
print(&header);
for row in &rows {
print(&row.each_ref().map(String::as_str));
}
println!(
"\n {} route(s), matched top to bottom · gateway: http://{}",
rows.len(),
apps.listen
);
Ok(())
}
pub fn matches(host: &str) -> Result<()> {
let apps = load()?;
let Some(found) = apps.routes.find(host) else {
bail!("no route matches `{host}`; the gateway answers 404");
};
let route = found.route;
println!(
" {host} → {} (route #{}) → {} ({})",
route.host,
found.index + 1,
if route.is_mcp { "mcp" } else { &route.app },
route.upstream
);
for (name, value) in &found.params {
println!(
" header: {}: {value}",
slave_gateway::routes::param_header(name)
);
}
if route.is_mcp {
println!(
" header: host: {} (rewritten for the MCP backend)",
route.upstream
);
}
Ok(())
}
fn is_up(upstream: &str) -> bool {
upstream
.to_socket_addrs()
.ok()
.and_then(|mut addrs| addrs.next())
.is_some_and(|addr| TcpStream::connect_timeout(&addr, Duration::from_millis(300)).is_ok())
}