//! Clap-derive subcommand for the daemon, plus a runner that wires everything //! together. use std::error::Error; use std::path::PathBuf; use clap::Subcommand; use tokio::net::TcpListener; use crate::session::DEFAULT_LIVENESS_INTERVAL; use crate::sidecar::SidecarStore; use crate::tmux::Tmux; use crate::worktree::WorktreeManager; use crate::{ default_config_path, default_state_dir, load_config, local_api, EnvOverrides, SessionManager, }; #[derive(Debug, Subcommand)] pub enum DaemonCmd { /// Start the daemon. Default when no subcommand is given. Run, } /// Dispatch a parsed daemon subcommand. `config_override` is the unified /// `--config` flag from the top-level CLI. pub async fn dispatch( config_override: Option, cmd: Option, ) -> Result<(), Box> { match cmd.unwrap_or(DaemonCmd::Run) { DaemonCmd::Run => run(config_override).await, } } async fn run(config_override: Option) -> Result<(), Box> { let config_path: PathBuf = match config_override { Some(p) => p, None => default_config_path()?, }; let config = load_config(&config_path, EnvOverrides::from_env())?; tracing::info!( hostname = %config.hostname, bind = %config.bind_addr, "loaded config" ); let _ = default_state_dir(); std::fs::create_dir_all(config.sidecar_dir())?; std::fs::create_dir_all(config.fifo_dir())?; // First-start hook auto-install (#106). Best-effort, before serving: a // session spawned right away should already carry the hook. if config.hooks_auto_install { match crate::hooks::auto_install(&config.home_dir) { crate::hooks::AutoInstallOutcome::Installed => { tracing::info!("installed claude-code activity hook (#106)"); } crate::hooks::AutoInstallOutcome::Failed => { tracing::warn!("hooks auto-install failed; run `maestro hooks install` manually"); } _ => {} } } // No worktree dir to pre-create: without an override the worktrees live // under their base repos, created on demand (#56). let sidecar = SidecarStore::new(config.sidecar_dir()); let worktree_mgr = WorktreeManager::new(config.worktree_root.clone()); let tmux = match config.tmux_tmpdir.clone() { Some(p) => Tmux::new().with_tmpdir(p), None => Tmux::new(), }; let retained = std::sync::Arc::new(crate::retained_store::RetainedStore::new( config.retained_worktree_dir(), )); let manager = SessionManager::new_with_replay( std::sync::Arc::new(tmux), sidecar, worktree_mgr, config.fifo_dir(), config.pty_replay_bytes, ) .with_spawn_history(crate::spawn_history::SpawnHistory::new( config.spawn_history_path(), config.spawn_history_days, )) // One store instance shared with the API handlers: forced kills (#81) // record here, `GET /api/worktrees` (#80) reads back from here. .with_retained_store((*retained).clone()); // Spawned sessions learn where to report back (#103). Derived from the // configured bind address; set before any spawn can happen. manager.set_daemon_url(format!("http://{}", config.bind_addr)); let recovered = manager.recover().await?; tracing::info!(recovered, "adopted existing tmux sessions"); manager.start_liveness_watcher(DEFAULT_LIVENESS_INTERVAL); let projects = std::sync::Arc::new(crate::projects::ProjectScanner::new( config.project_roots.clone(), config.project_scan_depth, PathBuf::from(&config.home_dir), )); projects.start_background_refresh(); let router = local_api::router(local_api::state_for( manager, config.hostname.clone(), config.home_dir.clone(), config.spawn_presets.clone(), projects, retained, )); let listener = TcpListener::bind(config.bind_addr).await?; let bound = listener.local_addr()?; tracing::info!(addr = %bound, "API listening"); let shutdown = async { let _ = tokio::signal::ctrl_c().await; tracing::info!("ctrl-c received — shutting down"); }; axum::serve(listener, router) .with_graceful_shutdown(shutdown) .await?; Ok(()) }