pub mod paths;
#[cfg(unix)]
pub mod client;
#[cfg(unix)]
pub mod lifecycle;
#[cfg(unix)]
pub mod protocol;
#[cfg(unix)]
pub mod registry;
#[cfg(unix)]
pub mod selection;
#[cfg(unix)]
pub mod server;
#[cfg(unix)]
pub mod service;
#[cfg(unix)]
pub mod services;
#[cfg(unix)]
pub mod single_instance;
#[cfg(all(unix, test))]
pub(crate) mod testutil;
#[cfg(target_os = "macos")]
pub mod launchd;
#[cfg(target_os = "linux")]
pub mod systemd;
#[cfg(all(target_os = "macos", feature = "menu-bar"))]
pub mod tray;
#[cfg(unix)]
use std::path::Path;
#[cfg(unix)]
use std::path::PathBuf;
#[cfg(unix)]
use std::sync::Arc;
#[cfg(unix)]
use anyhow::Result;
#[cfg(unix)]
use crate::browser::BridgeConfig;
#[cfg(unix)]
use crate::snowflake::SnowflakeEngineConfig;
#[cfg(unix)]
use registry::ServiceRegistry;
#[cfg(unix)]
pub use selection::{DaemonServiceKind, ServiceSelection};
#[cfg(unix)]
use server::DaemonOptions;
#[cfg(unix)]
use services::bridge::BridgeService;
#[cfg(unix)]
use services::github_counters::GithubCountersService;
#[cfg(unix)]
use services::sessions::SessionsService;
#[cfg(unix)]
use services::snowflake::SnowflakeService;
#[cfg(unix)]
use services::worktrees::WorktreesService;
#[cfg(unix)]
#[derive(Debug, Clone)]
pub struct DaemonRunConfig {
pub socket_path: PathBuf,
pub bridge_config: BridgeConfig,
pub bridge_token_file: Option<PathBuf>,
pub bridge_token_path: PathBuf,
pub services: ServiceSelection,
}
#[cfg(unix)]
pub async fn build_default_registry(
bridge_config: BridgeConfig,
bridge_token_file: Option<&Path>,
bridge_token_path: PathBuf,
services: &ServiceSelection,
) -> Result<ServiceRegistry> {
let mut registry = ServiceRegistry::new();
if services.includes(DaemonServiceKind::Bridge) {
let bridge =
BridgeService::start(bridge_config, bridge_token_file, bridge_token_path).await?;
registry.register(Arc::new(bridge));
}
if services.includes(DaemonServiceKind::Snowflake) {
let snowflake = SnowflakeService::new(SnowflakeEngineConfig::from_env_and_settings()?);
registry.register(Arc::new(snowflake));
}
if services.includes(DaemonServiceKind::Worktrees) {
let worktrees = WorktreesService::new();
match crate::daemon::paths::worktrees_polling_path() {
Ok(path) => worktrees.load_polling_prefs(path),
Err(err) => tracing::warn!("worktrees polling prefs disabled: {err:#}"),
}
match crate::daemon::paths::worktrees_pr_cache_path() {
Ok(path) => worktrees.load_pr_cache(path),
Err(err) => tracing::warn!("worktrees PR cache disabled: {err:#}"),
}
worktrees.start_menu_refresh();
worktrees.start_pr_poller();
worktrees.start_rate_limit_poller();
let rate_limit_cache = worktrees.rate_limit_cache();
registry.register(Arc::new(worktrees));
registry.set_github_rate_limit(rate_limit_cache);
}
if services.includes(DaemonServiceKind::Sessions) {
let sessions = SessionsService::new();
sessions.start_watcher();
registry.register(Arc::new(sessions));
}
let github_counters = GithubCountersService::new();
github_counters.start_counter_logger();
registry.register(Arc::new(github_counters));
Ok(registry)
}
#[cfg(unix)]
pub async fn run_headless(cfg: DaemonRunConfig) -> Result<()> {
let registry = build_default_registry(
cfg.bridge_config,
cfg.bridge_token_file.as_deref(),
cfg.bridge_token_path,
&cfg.services,
)
.await?;
server::run(
registry,
DaemonOptions {
socket_path: cfg.socket_path,
},
)
.await
}
#[cfg(all(unix, test))]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[tokio::test]
async fn build_default_registry_honours_a_subset() {
let selection = ServiceSelection::Only(vec![DaemonServiceKind::Snowflake]);
let registry = build_default_registry(
BridgeConfig::default(),
None,
PathBuf::from("/nonexistent/bridge.token"),
&selection,
)
.await
.expect("a snowflake-only registry builds without touching the bridge");
let names: Vec<_> = registry.services().iter().map(|s| s.name()).collect();
assert_eq!(names, vec![services::snowflake::SERVICE_NAME, "github"]);
}
}