use clap::Args;
use colored::Colorize;
use std::process::Command as StdCommand;
use tokio::signal;
#[derive(Args)]
pub struct DashboardArgs {
#[arg(long, short, default_value = "8080")]
pub port: u16,
#[arg(long)]
pub no_open: bool,
#[arg(long, env = "AGENTOVEN_SERVER_BIN")]
pub server_bin: Option<String>,
}
pub async fn execute(args: DashboardArgs) -> anyhow::Result<()> {
println!();
println!(" 🏺 {}", "AgentOven Dashboard".bold());
println!();
let port = args.port;
if is_server_running(port).await {
println!(
" ✅ Control plane already running on port {}",
port.to_string().cyan()
);
open_dashboard(port, args.no_open);
println!(
" 🌐 Dashboard: {}",
format!("http://localhost:{port}").underline().cyan()
);
println!();
return Ok(());
}
let server_bin = match args.server_bin {
Some(bin) => bin,
None => find_server_binary()?,
};
println!(
" 🚀 Starting control plane on port {}...",
port.to_string().cyan()
);
let mut cmd = StdCommand::new(&server_bin);
cmd.env("AGENTOVEN_PORT", port.to_string());
if std::env::var("AGENTOVEN_DASHBOARD_DIR").is_err() {
if let Some(dir) = find_dashboard_dir() {
cmd.env("AGENTOVEN_DASHBOARD_DIR", &dir);
}
}
let mut child = cmd.spawn().map_err(|e| {
anyhow::anyhow!("Failed to start control plane: {e}\n Binary: {server_bin}")
})?;
let ready = wait_for_server(port, 15).await;
if !ready {
child.kill().ok();
anyhow::bail!(
"Control plane did not start within 15 seconds.\n \
Check the server logs for errors."
);
}
println!(" 🔥 Control plane is hot and ready!");
open_dashboard(port, args.no_open);
println!(
" 🌐 Dashboard: {}",
format!("http://localhost:{port}").underline().cyan()
);
println!();
println!(" Press {} to stop.", "Ctrl+C".bold());
println!();
signal::ctrl_c().await?;
println!();
println!(" 🛑 Shutting down control plane...");
child.kill().ok();
child.wait().ok();
println!(" 👋 AgentOven stopped. Goodbye!");
println!();
Ok(())
}
async fn is_server_running(port: u16) -> bool {
let url = format!("http://localhost:{port}/health");
match reqwest::get(&url).await {
Ok(resp) => resp.status().is_success(),
Err(_) => false,
}
}
async fn wait_for_server(port: u16, timeout_secs: u64) -> bool {
let url = format!("http://localhost:{port}/health");
let start = std::time::Instant::now();
let timeout = std::time::Duration::from_secs(timeout_secs);
while start.elapsed() < timeout {
if let Ok(resp) = reqwest::get(&url).await {
if resp.status().is_success() {
return true;
}
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
false
}
fn open_dashboard(port: u16, no_open: bool) {
if no_open {
return;
}
let url = format!("http://localhost:{port}");
if let Err(e) = open_url(&url) {
eprintln!(" ⚠️ Could not open browser: {e}");
}
}
fn open_url(url: &str) -> Result<(), String> {
#[cfg(target_os = "macos")]
{
StdCommand::new("open")
.arg(url)
.spawn()
.map_err(|e| e.to_string())?;
}
#[cfg(target_os = "linux")]
{
StdCommand::new("xdg-open")
.arg(url)
.spawn()
.map_err(|e| e.to_string())?;
}
#[cfg(target_os = "windows")]
{
StdCommand::new("cmd")
.args(["/C", "start", url])
.spawn()
.map_err(|e| e.to_string())?;
}
Ok(())
}
fn find_server_binary() -> anyhow::Result<String> {
if which_exists("agentoven-server") {
return Ok("agentoven-server".to_string());
}
if which_exists("go") {
let candidates = [
"control-plane/cmd/server",
"../control-plane/cmd/server",
"../../control-plane/cmd/server",
];
for candidate in &candidates {
let path = std::path::Path::new(candidate);
if path.join("main.go").exists() {
let abs = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
println!(" 🔨 Building control plane...");
let build_status = StdCommand::new("go")
.args([
"build",
"-o",
"/tmp/agentoven-server",
&format!("./{}", candidate),
])
.status();
match build_status {
Ok(s) if s.success() => return Ok("/tmp/agentoven-server".to_string()),
_ => {
return Ok(format!("go run ./{}", abs.display()));
}
}
}
}
}
anyhow::bail!(
"Could not find the AgentOven control plane server.\n\n\
Options:\n\
• Install it: cargo install agentoven-server\n\
• Set the path: agentoven dashboard --server-bin /path/to/server\n\
• Set env: AGENTOVEN_SERVER_BIN=/path/to/server\n\
• Run from the repo root (we'll auto-detect control-plane/)"
)
}
fn which_exists(cmd: &str) -> bool {
StdCommand::new("which")
.arg(cmd)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn find_dashboard_dir() -> Option<String> {
use std::path::PathBuf;
let mut candidates: Vec<PathBuf> = Vec::new();
if let Ok(exe) = std::env::current_exe() {
let raw_dir = exe.parent().unwrap_or(&exe).to_path_buf();
candidates.push(
raw_dir
.join("..")
.join("share")
.join("agentoven")
.join("dashboard"),
);
if let Ok(resolved) = std::fs::canonicalize(&exe) {
let resolved_dir = resolved.parent().unwrap_or(&resolved).to_path_buf();
candidates.push(
resolved_dir
.join("..")
.join("share")
.join("agentoven")
.join("dashboard"),
);
}
}
candidates.push("dashboard/dist".into());
candidates.push("control-plane/dashboard/dist".into());
for candidate in &candidates {
let index = candidate.join("index.html");
if index.exists() {
if let Ok(abs) = std::fs::canonicalize(candidate) {
return Some(abs.to_string_lossy().to_string());
}
}
}
None
}