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 direct = apps
.routes
.iter()
.filter(|route| route.host.is_none() && route.path.is_none());
let rows: Vec<[String; 4]> = apps
.routes
.in_matching_order()
.map(|(_, route)| route)
.chain(direct)
.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.label(),
app,
route.upstream.clone(),
status.to_owned(),
]
})
.collect();
let header = ["ROUTE", "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));
}
let gateway = if crate::gateway::is_installed(&project::find_root()?) {
format!("gateway: http://{}", apps.listen)
} else {
"gateway not installed: routed entries are inactive (`slave install:gateway`)".to_owned()
};
println!(
"\n {} entr{}, most specific first · {gateway}",
rows.len(),
if rows.len() == 1 { "y" } else { "ies" }
);
Ok(())
}
pub fn matches(target: &str) -> Result<()> {
let apps = load()?;
let target = target
.strip_prefix("http://")
.or_else(|| target.strip_prefix("https://"))
.unwrap_or(target);
let (host, path) = match target.find('/') {
Some(at) => (&target[..at], &target[at..]),
None => (target, "/"),
};
let Some(found) = apps.routes.find(host, path) else {
bail!("no route matches `{target}`; the gateway answers 404");
};
let route = found.route;
println!(
" {target} → {} (route #{}) → {} ({})",
route.label(),
found.index + 1,
if route.is_mcp { "mcp" } else { &route.app },
route.upstream
);
if let Some(prefix) = &route.path {
println!(" path: forwarded unchanged; the app's base path is {prefix}");
}
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())
}