use clap::{Parser, Subcommand};
use std::path::PathBuf;
use std::time::Duration;
use tracing::{debug, error, info};
use aiecho::{discover_services, ClientConfig, DiscoveryScanner, DiscoveryServer};
#[derive(Parser)]
#[command(name = "aiecho")]
#[command(version = "0.1.0")]
#[command(about = "AI-LAN Service Discovery System", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Agent {
#[arg(short, long, default_value = ".", value_name = "DIR")]
root_path: PathBuf,
#[arg(short, long)]
verbose: bool,
#[arg(long)]
udp_port: Option<u16>,
},
Scan {
#[arg(short, long, default_value = "json")]
output: String,
#[arg(short, long, default_value = "2.0")]
timeout: f64,
#[arg(long)]
no_manifest: bool,
#[arg(short, long, value_name = "FILE")]
output_file: Option<PathBuf>,
#[arg(short, long)]
verbose: bool,
},
Listen {
#[arg(short, long, value_name = "FILE")]
output_file: PathBuf,
#[arg(short, long, default_value = "30")]
interval: u32,
#[arg(long)]
no_manifest: bool,
#[arg(short, long)]
verbose: bool,
},
}
#[tokio::main]
async fn main() {
let cli = Cli::parse();
let verbose = match &cli.command {
Commands::Agent { verbose, .. } => *verbose,
Commands::Scan { verbose, .. } => *verbose,
Commands::Listen { verbose, .. } => *verbose,
};
tracing_subscriber::fmt()
.with_max_level(if verbose {
tracing::Level::DEBUG
} else {
tracing::Level::INFO
})
.init();
match cli.command {
Commands::Agent {
root_path,
verbose: _,
udp_port,
} => {
run_agent(root_path, udp_port).await;
}
Commands::Scan {
output,
timeout,
no_manifest,
output_file,
verbose: _,
} => {
run_scan(output, timeout, no_manifest, output_file).await;
}
Commands::Listen {
output_file,
interval,
no_manifest,
verbose: _,
} => {
run_listen(output_file, interval, no_manifest).await;
}
}
}
async fn run_agent(root_path: PathBuf, udp_port: Option<u16>) {
info!("Scanning for .echo files in {}", root_path.display());
let services = discover_services(&root_path);
if services.is_empty() {
info!("No services found");
return;
}
info!("Found {} service(s)", services.len());
let mut servers = Vec::new();
for (echo_path, mut service_config) in services {
if let Some(port) = udp_port {
service_config.udp_port = port;
}
info!("Starting discovery agent: {}", service_config.service_name);
info!(" Service ID: {}", service_config.service_id);
info!(" HTTP Port: {}", service_config.http_port);
info!(" UDP Port: {}", service_config.udp_port);
info!(" From: {}", echo_path.display());
let mut server = DiscoveryServer::new(service_config);
if let Err(e) = server.start().await {
error!("Failed to start server: {}", e);
continue;
}
servers.push(server);
}
if servers.is_empty() {
error!("No servers started");
return;
}
info!("All agents started. Press Ctrl+C to stop.");
tokio::signal::ctrl_c()
.await
.expect("Failed to listen for ctrl+c");
info!("Stopping agents...");
for mut server in servers {
if let Err(e) = server.stop().await {
error!("Error stopping server: {}", e);
}
}
info!("All agents stopped.");
}
async fn run_scan(output: String, timeout: f64, no_manifest: bool, output_file: Option<PathBuf>) {
info!("Scanning for services...");
let config = ClientConfig {
timeout,
fetch_manifest: !no_manifest,
output_format: output.clone(),
..Default::default()
};
let scanner = DiscoveryScanner::new(config);
match scanner.scan(Some(!no_manifest)).await {
Ok(services) => {
if services.is_empty() {
info!("No services found.");
return;
}
info!("Found {} service(s)", services.len());
match output.as_str() {
"json" => {
let result: Vec<serde_json::Value> = services
.iter()
.map(|s| {
serde_json::json!({
"ip": s.ip(),
"port": s.port(),
"base_url": s.base_url(),
"manifest": s.manifest(),
})
})
.collect();
let json = serde_json::to_string_pretty(&result).unwrap();
if let Some(path) = output_file {
std::fs::write(&path, &json).unwrap();
info!("Output written to {}", path.display());
} else {
println!("{}", json);
}
}
"table" => {
println!("\n=== Discovered Services ===\n");
for s in &services {
println!(" {}:{}", s.ip(), s.port());
println!();
}
}
_ => {
error!("Unsupported output format: {}", output);
}
}
}
Err(e) => {
error!("Scan failed: {}", e);
std::process::exit(1);
}
}
}
async fn run_listen(output_file: PathBuf, interval: u32, no_manifest: bool) {
info!("Listening for service changes...");
info!(" Output file: {}", output_file.display());
info!(" Auto-scan interval: {}s", interval);
let config = ClientConfig {
scan_interval: interval,
fetch_manifest: !no_manifest,
..Default::default()
};
let scanner = DiscoveryScanner::new(config);
info!("Running initial scan...");
match scanner.scan(Some(!no_manifest)).await {
Ok(services) => {
if !services.is_empty() {
info!("Found {} service(s)", services.len());
}
}
Err(e) => {
error!("Initial scan failed: {}", e);
}
}
loop {
tokio::time::sleep(Duration::from_secs(interval as u64)).await;
match scanner.scan(Some(!no_manifest)).await {
Ok(services) => {
if !services.is_empty() {
let result: Vec<serde_json::Value> = services
.iter()
.map(|s| {
serde_json::json!({
"ip": s.ip(),
"port": s.port(),
"manifest": s.manifest(),
})
})
.collect();
let json = serde_json::to_string_pretty(&result).unwrap();
let _ = std::fs::write(&output_file, &json);
}
}
Err(e) => {
debug!("Periodic scan failed: {}", e);
}
}
}
}