use axum::{routing::get, Json, Router};
use serde_json::json;
use tracing::info;
pub async fn start_health_server(port: u16, bind_addr: &str) -> anyhow::Result<()> {
let app = Router::new().route("/health", get(health_handler));
let ip: std::net::IpAddr = bind_addr
.parse()
.unwrap_or(std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST));
let addr = std::net::SocketAddr::new(ip, port);
info!("Health server listening on {}", addr);
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app).await?;
Ok(())
}
async fn health_handler() -> Json<serde_json::Value> {
Json(json!({"status": "ok"}))
}
#[cfg(target_os = "linux")]
pub fn install_service() -> anyhow::Result<()> {
let exe = std::env::current_exe()?;
let working_dir = std::env::current_dir()?;
let unit = format!(
r#"[Unit]
Description=aidaemon - AI personal daemon
After=network.target
[Service]
Type=simple
ExecStart={}
WorkingDirectory={}
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
"#,
exe.display(),
working_dir.display()
);
let path = "/etc/systemd/system/aidaemon.service";
std::fs::write(path, unit)?;
println!("Service file written to {}", path);
println!("Run: sudo systemctl daemon-reload && sudo systemctl enable --now aidaemon");
Ok(())
}
#[cfg(target_os = "macos")]
pub const MACOS_BUNDLE_ID: &str = "ai.aidaemon";
#[cfg(target_os = "macos")]
pub fn install_service() -> anyhow::Result<()> {
use std::path::PathBuf;
use std::process::Command;
let exe = std::env::current_exe()?;
let working_dir = std::env::current_dir()?;
let home = std::env::var("HOME")?;
let log_dir = format!("{home}/Library/Logs/aidaemon");
std::fs::create_dir_all(&log_dir)?;
let app_dir = PathBuf::from(&home).join("Applications/aidaemon.app");
let macos_dir = app_dir.join("Contents/MacOS");
std::fs::create_dir_all(&macos_dir)?;
let bundle_bin = macos_dir.join("aidaemon");
std::fs::copy(&exe, &bundle_bin)?;
let info_plist = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key><string>{bundle_id}</string>
<key>CFBundleName</key><string>aidaemon</string>
<key>CFBundleExecutable</key><string>aidaemon</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleVersion</key><string>{version}</string>
<key>CFBundleShortVersionString</key><string>{version}</string>
<key>LSUIElement</key><true/>
<key>LSMinimumSystemVersion</key><string>13.0</string>
</dict>
</plist>
"#,
bundle_id = MACOS_BUNDLE_ID,
version = env!("CARGO_PKG_VERSION"),
);
std::fs::write(app_dir.join("Contents/Info.plist"), info_plist)?;
let have_identity = Command::new("security")
.args(["find-identity", "-v", "-p", "codesigning"])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).contains("aidaemon-dev"))
.unwrap_or(false);
let sign_arg = if have_identity { "aidaemon-dev" } else { "-" };
let sign = Command::new("codesign")
.args(["-f", "-s", sign_arg, "--identifier", MACOS_BUNDLE_ID])
.arg("--timestamp=none")
.arg(&app_dir)
.output()?;
if !sign.status.success() {
anyhow::bail!(
"codesign failed: {}",
String::from_utf8_lossy(&sign.stderr).trim()
);
}
let plist = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>{label}</string>
<key>ProgramArguments</key>
<array>
<string>{bin}</string>
</array>
<key>WorkingDirectory</key>
<string>{wd}</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>{log}/stdout.log</string>
<key>StandardErrorPath</key>
<string>{log}/stderr.log</string>
</dict>
</plist>
"#,
label = MACOS_BUNDLE_ID,
bin = bundle_bin.display(),
wd = working_dir.display(),
log = log_dir,
);
let path = format!("{home}/Library/LaunchAgents/ai.aidaemon.plist");
std::fs::write(&path, plist)?;
println!("Installed signed app bundle: {}", app_dir.display());
println!(
"Signed with {}.",
if have_identity {
"stable identity 'aidaemon-dev' (grants survive rebuilds)"
} else {
"ad-hoc signature (run scripts/create-signing-identity.sh for rebuild-durable grants)"
}
);
println!("Plist written to {path}");
println!("Logs will be written to {log_dir}/");
println!("Run: launchctl bootstrap gui/$(id -u) {path}");
println!("Then grant Accessibility + Screen Recording to aidaemon — see COMPUTER_USE_MACOS.md");
Ok(())
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
pub fn install_service() -> anyhow::Result<()> {
anyhow::bail!("Service installation is only supported on Linux and macOS");
}
#[cfg(target_os = "macos")]
pub fn spawn_macos_keep_awake() {
let pid = std::process::id();
match std::process::Command::new("/usr/bin/caffeinate")
.args(["-i", "-w", &pid.to_string()])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
{
Ok(_) => info!("macOS keep-awake active (caffeinate -i -w {pid})"),
Err(e) => tracing::warn!("Could not start caffeinate keep-awake: {e}"),
}
}