pitchfork-cli 2.6.0

Daemons with DX
Documentation
use crate::Result;
use crate::daemon::Daemon;
use crate::daemon_id::DaemonId;
use crate::daemon_status::DaemonStatus;
use crate::ipc::client::IpcClient;
use crate::pitchfork_toml::PitchforkToml;
use crate::settings::settings;
use std::collections::HashSet;

/// Represents a daemon entry that can be either tracked (from state file) or available (from config only)
#[derive(Debug, Clone)]
pub struct DaemonListEntry {
    pub id: DaemonId,
    pub daemon: Daemon,
    pub is_disabled: bool,
    pub is_available: bool, // true if daemon is only in config, not in state
}

/// Get a unified list of all daemons from IPC client and config
///
/// This function merges daemons from the state file (including failed daemons) with daemons
/// defined in config files. Daemons that are only in config (not in state file) are marked
/// as "available".
///
/// This logic is shared across:
/// - `pitchfork list` command
/// - TUI daemon list
///
/// # Arguments
/// * `client` - IPC client to communicate with supervisor (used only for disabled list)
///
/// # Returns
/// A vector of daemon entries with their current status
pub async fn get_all_daemons(client: &IpcClient) -> Result<Vec<DaemonListEntry>> {
    let config = PitchforkToml::all_merged()?;

    // Read state file to get all daemons (including failed ones)
    let state_file = crate::state_file::StateFile::read(&*crate::env::PITCHFORK_STATE_FILE)?;
    let state_daemons: Vec<Daemon> = state_file.daemons.values().cloned().collect();

    let disabled_daemons = client.get_disabled_daemons().await?;
    let disabled_set: HashSet<DaemonId> = disabled_daemons.into_iter().collect();

    build_daemon_list(state_daemons, disabled_set, config)
}

/// Get a unified list of all daemons from supervisor directly (for Web UI)
///
/// This function is used by the Web UI which runs inside the supervisor process
/// and can access the supervisor directly without IPC.
///
/// # Arguments
/// * `supervisor` - Reference to the supervisor instance
///
/// # Returns
/// A vector of daemon entries with their current status
pub async fn get_all_daemons_direct(
    supervisor: &crate::supervisor::Supervisor,
) -> Result<Vec<DaemonListEntry>> {
    let config = PitchforkToml::all_merged()?;

    // Read all daemons from state file (including failed/stopped ones)
    // Note: Don't use supervisor.active_daemons() as it only returns daemons with PIDs
    let state_file = supervisor.state_file.lock().await;
    let state_daemons: Vec<Daemon> = state_file.daemons.values().cloned().collect();
    let disabled_set: HashSet<DaemonId> = state_file.disabled.clone().into_iter().collect();
    drop(state_file); // Release lock early

    build_daemon_list(state_daemons, disabled_set, config)
}

/// Internal helper to build the daemon list from state daemons and config
fn build_daemon_list(
    state_daemons: Vec<Daemon>,
    disabled_set: HashSet<DaemonId>,
    config: PitchforkToml,
) -> Result<Vec<DaemonListEntry>> {
    let mut entries = Vec::new();
    let mut seen_ids = HashSet::new();

    // Skip the supervisor itself
    let pitchfork_id = DaemonId::pitchfork();

    // First, add all daemons from state file
    for daemon in state_daemons {
        if daemon.id == pitchfork_id {
            continue; // Skip supervisor itself
        }

        // proxy and mise are stored as Option<bool> in the Daemon struct.
        // None means "inherit from global settings", which is resolved at display/routing time.
        // No override needed here — daemon_list consumers call .unwrap_or(settings()...) themselves.

        seen_ids.insert(daemon.id.clone());
        entries.push(DaemonListEntry {
            id: daemon.id.clone(),
            is_disabled: disabled_set.contains(&daemon.id),
            is_available: false,
            daemon,
        });
    }

    // Then, add daemons from config that aren't in state file (available daemons)
    for (daemon_id, daemon_config) in &config.daemons {
        if *daemon_id == pitchfork_id || seen_ids.contains(daemon_id) {
            continue;
        }

        // Create a placeholder daemon for config-only entries
        let placeholder = Daemon {
            id: daemon_id.clone(),
            status: DaemonStatus::Stopped,
            port_bump_attempts: settings().default_port_bump_attempts(),
            depends: vec![],
            env: None,
            watch: vec![],
            watch_base_dir: None,
            mise: daemon_config.mise,
            active_port: None,
            slug: None,
            proxy: None,
            memory_limit: daemon_config.memory_limit,
            cpu_limit: daemon_config.cpu_limit,
            ..Daemon::default()
        };

        entries.push(DaemonListEntry {
            id: daemon_id.clone(),
            daemon: placeholder,
            is_disabled: disabled_set.contains(daemon_id),
            is_available: true,
        });
    }

    Ok(entries)
}