use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use bitrouter::commands;
use bitrouter::daemon::{self, DaemonCommand, DaemonResponse, RouteHop};
use bitrouter_sdk::config;
#[derive(Parser)]
#[command(name = "bitrouter", version, about)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Serve {
#[arg(short, long)]
config: Option<PathBuf>,
},
Start {
#[arg(short, long)]
config: Option<PathBuf>,
#[arg(long)]
log: Option<PathBuf>,
},
Stop {
#[arg(short, long)]
config: Option<PathBuf>,
#[arg(long)]
socket: Option<PathBuf>,
},
Restart {
#[arg(short, long)]
config: Option<PathBuf>,
#[arg(long)]
socket: Option<PathBuf>,
#[arg(long)]
log: Option<PathBuf>,
},
Reload {
#[arg(short, long)]
config: Option<PathBuf>,
#[arg(long)]
socket: Option<PathBuf>,
},
Status {
#[arg(short, long)]
config: Option<PathBuf>,
#[arg(long)]
socket: Option<PathBuf>,
},
Route {
model: String,
#[arg(short, long)]
config: Option<PathBuf>,
#[arg(long)]
socket: Option<PathBuf>,
},
Init {
#[arg(short, long, default_value = "bitrouter.yaml")]
config: PathBuf,
},
Key {
#[command(subcommand)]
action: KeyAction,
},
Models {
#[arg(short, long)]
config: Option<PathBuf>,
#[arg(short, long)]
provider: Option<String>,
},
Tools {
#[command(subcommand)]
action: ToolsAction,
},
Policy {
#[command(subcommand)]
action: PolicyAction,
},
Providers {
#[command(subcommand)]
action: ProviderAction,
},
Wallet,
Login {
provider: Option<String>,
},
Logout {
provider: Option<String>,
},
Whoami,
Agents {
#[command(subcommand)]
action: AgentsAction,
},
#[command(name = "agent-proxy")]
AgentProxy {
agent: String,
#[arg(short, long)]
config: Option<PathBuf>,
},
}
#[derive(Subcommand)]
enum AgentsAction {
List {
#[arg(short, long)]
config: Option<PathBuf>,
},
Check {
#[arg(short, long)]
config: Option<PathBuf>,
},
Install {
id: String,
},
}
#[derive(Subcommand)]
enum ToolsAction {
List {
#[arg(short, long)]
config: Option<PathBuf>,
},
Status {
#[arg(short, long)]
config: Option<PathBuf>,
},
Discover {
server: String,
#[arg(short, long)]
config: Option<PathBuf>,
},
}
#[derive(Subcommand)]
enum KeyAction {
Sign {
#[arg(short, long)]
user: String,
#[arg(short, long, default_value = "sqlite://./bitrouter.db")]
db: String,
#[arg(long)]
policy: Option<String>,
},
}
#[derive(Subcommand)]
enum PolicyAction {
Create {
id: String,
#[arg(long, default_value = "./policies")]
dir: PathBuf,
},
}
#[derive(Subcommand)]
enum ProviderAction {
List {
#[arg(short, long)]
config: Option<PathBuf>,
},
Use {
id: String,
},
}
#[tokio::main]
async fn main() {
match run().await {
Ok(()) => {}
Err(e) => {
bitrouter::error_report::report(&e);
std::process::exit(1);
}
}
}
async fn run() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.init();
let cli = Cli::parse();
match cli.command {
Command::Serve { config } => {
let source = bitrouter::paths::resolve_config(config.as_deref())?;
serve(&source).await
}
Command::Start { config, log } => {
let source = bitrouter::paths::resolve_config(config.as_deref())?;
let log_path = resolve_log_path(source.home(), log.as_deref());
start(&source, &log_path).await
}
Command::Stop { config, socket } => {
let socket = resolve_client_socket(config.as_deref(), socket.as_deref()).await?;
stop(&socket).await
}
Command::Restart {
config,
socket,
log,
} => {
let source = bitrouter::paths::resolve_config(config.as_deref())?;
let socket = resolve_client_socket_from(&source, socket.as_deref()).await?;
let log_path = resolve_log_path(source.home(), log.as_deref());
restart(&source, &socket, &log_path).await
}
Command::Reload { config, socket } => {
let socket = resolve_client_socket(config.as_deref(), socket.as_deref()).await?;
reload(&socket).await
}
Command::Status { config, socket } => {
let socket = resolve_client_socket(config.as_deref(), socket.as_deref()).await?;
status(&socket).await
}
Command::Route {
model,
config,
socket,
} => {
let source = bitrouter::paths::resolve_config(config.as_deref())?;
let socket = resolve_client_socket_from(&source, socket.as_deref()).await?;
route(&model, &source, &socket).await
}
Command::Init { config } => init(&config).await,
Command::Key { action } => key(action).await,
Command::Models { config, provider } => {
let source = bitrouter::paths::resolve_config(config.as_deref())?;
models(&source, provider.as_deref()).await
}
Command::Tools { action } => tools(action).await,
Command::Policy { action } => policy(action).await,
Command::Providers { action } => providers(action).await,
Command::Wallet => {
print_unimplemented(
"wallet",
"OWS wallet integration is delivered by the `ows` workspace, not\n\
by bitrouter. v1.0 ships the routing / settlement layer only.",
);
Ok(())
}
Command::Login { provider } => match provider.as_deref() {
Some(name) => bitrouter::commands::login_provider(name).await,
None => {
print_unimplemented(
"login",
"Cloud login is not in v1.0 scope. Run\n\
`bitrouter login <provider>` for per-provider OAuth (today: github-copilot),\n\
or mint a local virtual key with `bitrouter key sign --user <id>`.",
);
Ok(())
}
},
Command::Logout { provider } => match provider.as_deref() {
Some(name) => bitrouter::commands::logout_provider(name).await,
None => {
print_unimplemented(
"logout",
"See `bitrouter logout <provider>` for per-provider OAuth logout.\n\
No cloud session in v1.0.",
);
Ok(())
}
},
Command::Whoami => {
print_unimplemented(
"whoami",
"Cloud identity is not in v1.0 scope. Local callers are identified\n\
by the `brvk_` virtual key (see `bitrouter key sign`).",
);
Ok(())
}
Command::Agents { action } => agents_cmd(action).await,
Command::AgentProxy { agent, config } => {
let source = bitrouter::paths::resolve_config(config.as_deref())?;
agent_proxy_cmd(&agent, &source).await
}
}
}
async fn resolve_client_socket(config: Option<&Path>, socket: Option<&Path>) -> Result<PathBuf> {
if let Some(s) = socket {
return Ok(s.to_path_buf());
}
let source = bitrouter::paths::resolve_config(config)?;
match &source {
bitrouter::paths::ConfigSource::File(path) => {
let socket_str = match config::load(path).await {
Ok(cfg) => cfg.server.control_socket,
Err(_) => daemon::DEFAULT_CONTROL_SOCKET.to_string(),
};
Ok(daemon::resolve_socket_path(path, &socket_str))
}
bitrouter::paths::ConfigSource::Default { home } => Ok(home.join("bitrouter.sock")),
}
}
fn resolve_log_path(home: &Path, log: Option<&Path>) -> PathBuf {
if let Some(l) = log {
return l.to_path_buf();
}
home.join("bitrouter.log")
}
async fn resolve_client_socket_from(
source: &bitrouter::paths::ConfigSource,
socket: Option<&Path>,
) -> Result<PathBuf> {
if let Some(s) = socket {
return Ok(s.to_path_buf());
}
match source {
bitrouter::paths::ConfigSource::File(path) => {
let cfg = config::load(path)
.await
.with_context(|| format!("loading {}", path.display()))?;
Ok(daemon::resolve_socket_path(
path,
&cfg.server.control_socket,
))
}
bitrouter::paths::ConfigSource::Default { home } => Ok(home.join("bitrouter.sock")),
}
}
async fn serve(source: &bitrouter::paths::ConfigSource) -> Result<()> {
let home = source.home();
bitrouter::paths::ensure_home_directory(home)?;
std::env::set_current_dir(home)
.with_context(|| format!("chdir to bitrouter home {}", home.display()))?;
let cfg = bitrouter::paths::load_config(source).await?;
announce_zero_config(source, &cfg);
let listen = cfg.server.listen.clone();
let socket_path = match source {
bitrouter::paths::ConfigSource::File(path) => {
daemon::resolve_socket_path(path, &cfg.server.control_socket)
}
bitrouter::paths::ConfigSource::Default { home } => home.join("bitrouter.sock"),
};
let pid_path = pid_path_for(&socket_path);
let config_path_for_reload = match source {
bitrouter::paths::ConfigSource::File(path) => Some(path.as_path()),
bitrouter::paths::ConfigSource::Default { .. } => None,
};
let assembled = bitrouter::build_app_with_path(&cfg, config_path_for_reload).await?;
let app = Arc::new(assembled.app);
let policy_store = assembled.policy_store;
let reload_source = match source {
bitrouter::paths::ConfigSource::File(path) => {
bitrouter::reload::ReloadSource::File(path.clone())
}
bitrouter::paths::ConfigSource::Default { .. } => bitrouter::reload::ReloadSource::Default,
};
let reloader: Arc<dyn daemon::DaemonReloader> = Arc::new(bitrouter::reload::AppReloader::new(
policy_store.clone(),
assembled.routing_table,
reload_source,
));
daemon::write_pid_file(&pid_path).await?;
println!(
"bitrouter {} — serving on {listen} (control: {})",
bitrouter::VERSION,
socket_path.display()
);
let http_app = app.clone();
let http_listen = listen.clone();
let http = async move {
http_app
.serve(&http_listen)
.await
.map_err(anyhow::Error::from)
};
let control = daemon::run_control_socket(socket_path, app.clone(), listen, reloader.clone());
let hup_reloader = reloader.clone();
let hup = async move {
use tokio::signal::unix::{SignalKind, signal};
let mut hup = match signal(SignalKind::hangup()) {
Ok(s) => s,
Err(e) => return Err::<(), _>(anyhow::Error::from(e)),
};
loop {
if hup.recv().await.is_none() {
return Ok(());
}
match hup_reloader.reload().await {
Ok(()) => tracing::info!("SIGHUP — reload succeeded"),
Err(e) => tracing::warn!(error = %e, "SIGHUP reload failed"),
}
}
};
let result = tokio::select! {
r = http => r,
r = control => r,
r = hup => match r {
Ok(()) => Ok(()),
Err(e) => { tracing::warn!(error = %e, "SIGHUP listener unavailable"); Ok(()) }
},
};
daemon::remove_pid_file(&pid_path).await;
result
}
async fn start(source: &bitrouter::paths::ConfigSource, log_path: &Path) -> Result<()> {
bitrouter::paths::ensure_home_directory(source.home())?;
let cfg_socket_path: Option<PathBuf> = match source {
bitrouter::paths::ConfigSource::File(path) => match config::load(path).await {
Ok(cfg) => Some(daemon::resolve_socket_path(
path,
&cfg.server.control_socket,
)),
Err(_) => None,
},
bitrouter::paths::ConfigSource::Default { home } => Some(home.join("bitrouter.sock")),
};
if let Some(socket) = &cfg_socket_path {
let pid_path = pid_path_for(socket);
if let Some(pid) = daemon::read_pid_file(&pid_path).await {
if process_is_alive(pid) {
anyhow::bail!(
"bitrouter is already running (pid {pid}); use `restart` or `stop` first"
);
}
daemon::remove_pid_file(&pid_path).await;
}
}
let exe = std::env::current_exe().context("locating current bitrouter binary")?;
let log_size_before = std::fs::metadata(log_path).map(|m| m.len()).unwrap_or(0);
let log = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(log_path)
.with_context(|| format!("opening daemon log {}", log_path.display()))?;
let log_err = log
.try_clone()
.context("duplicating log handle for stderr")?;
use std::os::unix::process::CommandExt;
let mut cmd = std::process::Command::new(&exe);
cmd.arg("serve");
if let bitrouter::paths::ConfigSource::File(path) = source {
cmd.arg("--config").arg(path);
}
let mut child = cmd
.stdout(std::process::Stdio::from(log))
.stderr(std::process::Stdio::from(log_err))
.stdin(std::process::Stdio::null())
.process_group(0)
.spawn()
.context("spawning detached `bitrouter serve`")?;
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
if let Ok(Some(status)) = child.try_wait() {
let tail = read_log_since(log_path, log_size_before).await;
eprint_failure_log(log_path, &tail);
anyhow::bail!("daemon exited immediately ({status})");
}
println!(
"bitrouter daemon started (pid {}) — logs at {}",
child.id(),
log_path.display()
);
Ok(())
}
fn announce_zero_config(
source: &bitrouter::paths::ConfigSource,
cfg: &bitrouter_sdk::config::Config,
) {
if !source.is_default() {
return;
}
let enabled: Vec<&str> = cfg.providers.keys().map(String::as_str).collect();
if enabled.is_empty() {
let mut hint: Vec<String> = bitrouter_providers::zero_config_env_var_providers()
.into_iter()
.map(|(_, env)| env.to_string())
.collect();
hint.sort();
hint.dedup();
bitrouter::error_report::info(format_args!(
"zero-config mode — no provider env vars set (try {})",
hint.join(" / ")
));
} else {
bitrouter::error_report::info(format_args!(
"zero-config mode — auto-enabled providers: {}",
enabled.join(", ")
));
}
}
async fn read_log_since(path: &Path, offset: u64) -> String {
let bytes = match tokio::fs::read(path).await {
Ok(b) => b,
Err(_) => return String::new(),
};
let start = (offset as usize).min(bytes.len());
String::from_utf8_lossy(&bytes[start..]).into_owned()
}
fn eprint_failure_log(log_path: &Path, content: &str) {
let trimmed = content.trim_end();
if trimmed.is_empty() {
return;
}
let p = bitrouter::style::Palette::for_stderr();
eprintln!(
"{dim}daemon log ({path}):{reset}",
dim = p.dim,
path = log_path.display(),
reset = p.reset,
);
for line in trimmed.lines() {
eprintln!(" {line}");
}
eprintln!();
}
async fn stop(socket: &Path) -> Result<()> {
match daemon::send_command(socket, &DaemonCommand::Stop).await? {
DaemonResponse::Ok => {
println!("daemon stopped");
Ok(())
}
DaemonResponse::Error { message } => Err(anyhow::anyhow!(message)),
other => Err(anyhow::anyhow!("unexpected response: {other:?}")),
}
}
async fn restart(
source: &bitrouter::paths::ConfigSource,
socket: &Path,
log_path: &Path,
) -> Result<()> {
if socket.exists() {
match daemon::send_command(socket, &DaemonCommand::Stop).await {
Ok(DaemonResponse::Ok) => println!("daemon stopped"),
Ok(DaemonResponse::Error { message }) => return Err(anyhow::anyhow!(message)),
Ok(other) => return Err(anyhow::anyhow!("unexpected response: {other:?}")),
Err(e) => tracing::warn!(error = %e, "stop failed — proceeding to start"),
}
let pid_path = pid_path_for(socket);
if !wait_for_socket_release(socket, std::time::Duration::from_secs(30)).await {
tracing::warn!("socket still held after 30s — escalating to SIGKILL on pid file");
if let Some(pid) = daemon::read_pid_file(&pid_path).await {
force_kill(pid).await;
}
wait_for_socket_release(socket, std::time::Duration::from_secs(2)).await;
daemon::remove_pid_file(&pid_path).await;
}
}
start(source, log_path).await
}
async fn wait_for_socket_release(socket: &Path, timeout: std::time::Duration) -> bool {
let deadline = std::time::Instant::now() + timeout;
while std::time::Instant::now() < deadline {
if !socket.exists() {
return true;
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
!socket.exists()
}
async fn reload(socket: &Path) -> Result<()> {
let env: Vec<(String, String)> = bitrouter_providers::zero_config_env_var_providers()
.into_iter()
.filter_map(|(_, var)| {
std::env::var(var)
.ok()
.filter(|v| !v.is_empty())
.map(|v| (var.to_string(), v))
})
.collect();
match daemon::send_command(socket, &DaemonCommand::Reload { env }).await? {
DaemonResponse::Ok => {
println!("config reloaded");
Ok(())
}
DaemonResponse::Error { message } => Err(anyhow::anyhow!(message)),
other => Err(anyhow::anyhow!("unexpected response: {other:?}")),
}
}
async fn status(socket: &Path) -> Result<()> {
let p = bitrouter::style::Palette::for_stdout();
match daemon::send_command(socket, &DaemonCommand::Status).await {
Ok(DaemonResponse::Status {
pid,
listen,
models,
}) => {
print_status_running(&p, pid, &listen, models, socket);
Ok(())
}
Ok(DaemonResponse::Error { message }) => Err(anyhow::anyhow!(message)),
Ok(other) => Err(anyhow::anyhow!("unexpected response: {other:?}")),
Err(e) if daemon::is_not_reachable(&e) => {
print_status_stopped(&p, socket);
Ok(())
}
Err(e) => Err(e),
}
}
fn print_status_running(
p: &bitrouter::style::Palette,
pid: u32,
listen: &str,
models: usize,
socket: &Path,
) {
println!(
"{green}●{reset} bitrouter is {bold}running{reset}",
green = p.green,
bold = p.bold,
reset = p.reset,
);
println!();
print_status_row(p, "pid", &pid.to_string());
print_status_row(p, "listen", listen);
print_status_row(p, "models", &format!("{models} routable"));
print_status_row(p, "socket", &socket.display().to_string());
}
fn print_status_stopped(p: &bitrouter::style::Palette, socket: &Path) {
println!(
"{dim}○{reset} bitrouter is {bold}stopped{reset}",
dim = p.dim,
bold = p.bold,
reset = p.reset,
);
println!();
print_status_row(p, "socket", &socket.display().to_string());
println!();
println!(
" {dim}Run `bitrouter start` to launch the daemon.{reset}",
dim = p.dim,
reset = p.reset,
);
}
fn print_status_row(p: &bitrouter::style::Palette, label: &str, value: &str) {
println!(
" {dim}{label:<8}{reset} {value}",
dim = p.dim,
reset = p.reset,
);
}
async fn route(model: &str, source: &bitrouter::paths::ConfigSource, socket: &Path) -> Result<()> {
if socket.exists() {
match daemon::send_command(
socket,
&DaemonCommand::Route {
model: model.into(),
},
)
.await
{
Ok(DaemonResponse::Route { chain }) => {
print_route_chain(model, &chain, "live daemon");
return Ok(());
}
Ok(DaemonResponse::Error { message }) => return Err(anyhow::anyhow!(message)),
Ok(other) => return Err(anyhow::anyhow!("unexpected response: {other:?}")),
Err(e) => {
tracing::debug!(error = %e, "daemon route failed — resolving from config");
}
}
}
let cfg = bitrouter::paths::load_config(source).await?;
let chain = commands::resolve_route(&cfg, model).await?;
let label = if source.is_default() {
"zero-config"
} else {
"config"
};
print_route_chain(model, &chain, label);
Ok(())
}
fn print_route_chain(model: &str, chain: &[RouteHop], source: &str) {
println!("model: {model} (resolved via: {source})");
if chain.is_empty() {
println!(" (empty chain — no provider declares this model)");
return;
}
for (i, hop) in chain.iter().enumerate() {
println!(
" {}. {} → {} ({})",
i + 1,
hop.provider,
hop.service_id,
hop.api_protocol
);
}
}
async fn init(config_path: &Path) -> Result<()> {
commands::init(config_path).await?;
println!("wrote starter config to {}", config_path.display());
println!(" (skip_auth is on — credential-less local requests are admitted)");
Ok(())
}
async fn key(action: KeyAction) -> Result<()> {
match action {
KeyAction::Sign { user, db, policy } => {
let key = commands::key_sign(&db, &user, policy.as_deref()).await?;
println!("created virtual key {} for user '{user}'", key.id);
println!();
println!(" {}", key.secret);
println!();
println!("This secret is shown ONCE — only its SHA-256 hash is stored.");
Ok(())
}
}
}
async fn models(source: &bitrouter::paths::ConfigSource, provider: Option<&str>) -> Result<()> {
let cfg = bitrouter::paths::load_config(source).await?;
let models = commands::list_models(&cfg, provider).await?;
if models.is_empty() {
match (provider, source.is_default()) {
(Some(p), _) => println!("(no routable models for provider '{p}')"),
(None, true) => {
println!("(no routable models — zero-config mode and no provider env vars are set)")
}
(None, false) => println!("(no routable models — configure providers in your config)"),
}
}
for (id, providers) in models {
println!("{id}\t{}", providers.join(", "));
}
Ok(())
}
async fn policy(action: PolicyAction) -> Result<()> {
match action {
PolicyAction::Create { id, dir } => {
let path = commands::create_policy(&dir, &id).await?;
println!("wrote starter policy to {}", path.display());
println!(" edit, then bind to a key with:");
println!(" bitrouter key sign --user <id> --policy {id}");
Ok(())
}
}
}
async fn providers(action: ProviderAction) -> Result<()> {
match action {
ProviderAction::List { config } => {
let source = bitrouter::paths::resolve_config(config.as_deref())?;
let cfg = bitrouter::paths::load_config(&source).await?;
let providers = commands::list_providers(&cfg);
if providers.is_empty() {
if source.is_default() {
println!("(no providers — zero-config mode and no provider env vars set)");
} else {
println!("(no providers configured)");
}
return Ok(());
}
println!("{:<20} {:<8} {:<6} API_BASE", "ID", "MODELS", "ACTIVE");
for p in providers {
println!(
"{:<20} {:<8} {:<6} {}",
p.id,
p.model_count,
if p.active { "yes" } else { "no" },
p.api_base
);
}
Ok(())
}
ProviderAction::Use { id } => {
println!("v1 has no \"current provider\" — `providers use {id}` is a no-op.");
println!(
" request a specific provider per-call via the model name or routing\n prefs; bind a default by editing `bitrouter.yaml`."
);
Ok(())
}
}
}
async fn tools(action: ToolsAction) -> Result<()> {
use bitrouter::tools as tools_cmd;
match action {
ToolsAction::List { config } => {
let source = bitrouter::paths::resolve_config(config.as_deref())?;
let cfg = bitrouter::paths::load_config(&source).await?;
if cfg.mcp_servers.is_empty() {
println!("(no MCP servers configured)");
println!(" add an `mcp_servers:` block to your bitrouter.yaml —");
println!(" see the commented stub in the starter config written by");
println!(" `bitrouter init`.");
return Ok(());
}
let rows = tools_cmd::list(&cfg).await;
for row in rows {
match row.outcome {
Ok(tools) if tools.is_empty() => {
println!("{} (no tools advertised)", row.server);
}
Ok(tools) => {
println!("{} ({})", row.server, tools.len());
for t in tools {
if t.description.is_empty() {
println!(" {}", t.name);
} else {
println!(" {} — {}", t.name, t.description);
}
}
}
Err(e) => {
eprintln!("{}: ERROR — {e}", row.server);
}
}
}
Ok(())
}
ToolsAction::Status { config } => {
let source = bitrouter::paths::resolve_config(config.as_deref())?;
let cfg = bitrouter::paths::load_config(&source).await?;
if cfg.mcp_servers.is_empty() {
println!("(no MCP servers configured)");
return Ok(());
}
let rows = tools_cmd::status(&cfg).await;
println!(
"{:<24} {:<8} {:<12} TRANSPORT",
"SERVER", "STATUS", "LATENCY"
);
for row in rows {
let (status, latency) = match row.outcome {
Ok(d) => ("ok".to_string(), format!("{}ms", d.as_millis())),
Err(_) => ("FAIL".to_string(), "-".to_string()),
};
println!(
"{:<24} {:<8} {:<12} {}",
row.server, status, latency, row.transport
);
if let Err(e) = row.outcome.as_ref() {
eprintln!(" ↳ {e}");
}
}
Ok(())
}
ToolsAction::Discover { server, config } => {
let source = bitrouter::paths::resolve_config(config.as_deref())?;
let cfg = bitrouter::paths::load_config(&source).await?;
match tools_cmd::discover(&cfg, &server).await {
Ok(yaml) => {
print!("{yaml}");
Ok(())
}
Err(e) => anyhow::bail!("discover '{server}': {e}"),
}
}
}
}
async fn agent_proxy_cmd(agent: &str, source: &bitrouter::paths::ConfigSource) -> Result<()> {
let cfg = bitrouter::paths::load_config(source).await?;
bitrouter::agent_proxy::run(cfg, agent).await
}
async fn agents_cmd(action: AgentsAction) -> Result<()> {
use bitrouter::agents as agents_cmd;
match action {
AgentsAction::List { config } => {
let source = bitrouter::paths::resolve_config(config.as_deref())?;
let cfg = bitrouter::paths::load_config(&source).await?;
let rows = agents_cmd::list(&cfg);
println!(
"{:<16} {:<12} {:<10} DESCRIPTION",
"ID", "CONFIGURED", "CATALOG"
);
for row in rows {
println!(
"{:<16} {:<12} {:<10} {}",
row.id,
if row.configured { "yes" } else { "no" },
if row.in_catalog { "yes" } else { "no" },
row.description,
);
}
Ok(())
}
AgentsAction::Check { config } => {
let source = bitrouter::paths::resolve_config(config.as_deref())?;
let cfg = bitrouter::paths::load_config(&source).await?;
if cfg.agents.is_empty() {
println!("(no agents configured)");
println!(" install one with: bitrouter agents install <id>");
return Ok(());
}
let rows = agents_cmd::check(&cfg).await;
println!("{:<24} {:<8} LATENCY/ERROR", "AGENT", "STATUS");
for row in rows {
match row.outcome {
Ok(d) => println!("{:<24} {:<8} {}ms", row.id, "ok", d.as_millis()),
Err(e) => {
println!("{:<24} {:<8} -", row.id, "FAIL");
eprintln!(" ↳ {e}");
}
}
}
Ok(())
}
AgentsAction::Install { id } => match agents_cmd::install(&id) {
Ok(yaml) => {
print!("{yaml}");
Ok(())
}
Err(e) => anyhow::bail!(e),
},
}
}
fn pid_path_for(socket: &Path) -> PathBuf {
let mut p = socket.to_path_buf();
p.set_extension("pid");
p
}
fn print_unimplemented(name: &str, detail: &str) {
eprintln!("`bitrouter {name}` is not implemented in v1.0.");
eprintln!();
for line in detail.lines() {
eprintln!(" {line}");
}
}
fn process_is_alive(pid: u32) -> bool {
std::process::Command::new("kill")
.args(["-0", &pid.to_string()])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
async fn force_kill(pid: u32) {
let _ = tokio::process::Command::new("kill")
.args(["-9", &pid.to_string()])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.await;
}