use shunt::term::{bold_white, brand_green, cyan, dim};
fn main() -> anyhow::Result<()> {
let args: Vec<String> = std::env::args().collect();
let is_start = args.iter().any(|a| a == "start");
let is_foreground = args.iter().any(|a| a == "--foreground");
let is_daemon = args.iter().any(|a| a == "--daemon");
if is_start && !is_daemon {
preflight_kill();
if !is_foreground {
spawn_daemon();
}
}
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?
.block_on(shunt::cli::run())
}
fn preflight_kill() {
let pid_path = shunt::config::pid_path();
let Ok(content) = std::fs::read_to_string(&pid_path) else { return };
let Ok(old_pid) = content.trim().parse::<u32>() else { return };
if old_pid == std::process::id() { return; }
if !pid_is_shunt(old_pid) { return; }
unsafe { libc::kill(old_pid as i32, libc::SIGKILL) };
std::thread::sleep(std::time::Duration::from_millis(400));
}
fn pid_is_shunt(pid: u32) -> bool {
let Ok(out) = std::process::Command::new("ps")
.args(["-p", &pid.to_string(), "-o", "comm="])
.output()
else {
return false;
};
let comm = String::from_utf8_lossy(&out.stdout);
comm.trim().contains("shunt")
}
fn spawn_daemon() {
let exe = match std::env::current_exe() {
Ok(p) => p,
Err(e) => {
eprintln!("Warning: cannot locate executable ({e}), starting in foreground");
return; }
};
let log_path = shunt::config::log_path();
if let Some(parent) = log_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let log_file = match std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
{
Ok(f) => f,
Err(e) => {
eprintln!("Warning: cannot open log file ({e}), starting in foreground");
return;
}
};
let mut child_args: Vec<String> = std::env::args()
.skip(1) .collect();
if !child_args.iter().any(|a| a == "--daemon") {
child_args.push("--daemon".into());
}
use std::os::unix::process::CommandExt;
let log_file2 = log_file.try_clone().ok();
let result = unsafe {
std::process::Command::new(&exe)
.args(&child_args)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::from(log_file))
.stderr(std::process::Stdio::from(log_file2.unwrap_or_else(|| {
std::fs::OpenOptions::new()
.create(true).append(true).open(&log_path).unwrap()
})))
.pre_exec(|| {
libc::setsid();
Ok(())
})
.spawn()
};
match result {
Ok(_child) => {
let addrs = load_addrs();
println!();
println!(" {} {} {}",
brand_green("◆"),
bold_white("shunt"),
bold_white("started"));
for (provider, addr) in &addrs {
let label = format!("{provider:<12}");
println!(" {} {} {}", dim("·"), dim(&label), cyan(addr));
}
println!(" {} run {} for account details",
dim("·"), cyan("shunt status"));
println!();
std::process::exit(0);
}
Err(e) => {
eprintln!("Warning: could not daemonize ({e}), starting in foreground");
}
}
}
fn load_addrs() -> Vec<(String, String)> {
use shunt::provider::Provider;
let Ok(cfg) = shunt::config::load_config(None) else {
return vec![("anthropic".into(), "http://127.0.0.1:8082".into())];
};
let host = &cfg.server.host;
let primary_port = cfg.server.port;
use std::collections::BTreeSet;
let providers: BTreeSet<String> = cfg.accounts.iter()
.map(|a| a.provider.to_string())
.collect();
providers.into_iter().map(|p| {
let port = match Provider::from_str(&p) {
Provider::Anthropic => primary_port,
other => other.default_port(),
};
(p, format!("http://{host}:{port}"))
}).collect()
}