loopflow 0.9.11

Run steps and flows with coding agents
Documentation
mod compose;
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "macos")]
mod macos;
mod onboarding;

use std::path::Path;

use anyhow::Result;

use crate::lfd::config::{LfdConfig, ServiceManager};

const SERVICE_ENV_KEYS: &[&str] = &[
    "LFD_HTTP_ADDR",
    "LFD_AUTH_TOKEN",
    "LFD_MODE",
    "LFD_DB_PATH",
    "LFD_DATABASE_URL",
    "LFD_MAX_SLOTS",
    "LFD_GITHUB_WEBHOOK_SECRET",
    "LFD_GITHUB_TOKEN",
    "LFD_EXECUTOR_CREDENTIALS_ENV",
    "LFD_EXECUTOR_CREDENTIALS_MOUNTS",
    "LFD_EXECUTOR_IMAGE",
    "LFD_EXECUTOR_AGENT_TIMEOUT",
    "LFD_EXECUTOR_LIMITS_MEMORY",
    "LFD_EXECUTOR_LIMITS_MEMORY_SWAP",
    "LFD_EXECUTOR_LIMITS_CPU_QUOTA",
    "LFD_EXECUTOR_LIMITS_PIDS_LIMIT",
    "LFD_HTTP_MAX_JSON_BODY_BYTES",
    "LFD_HTTP_MAX_HOOK_BODY_BYTES",
    "LFD_HTTP_MAX_WS_FRAME_BYTES",
    "LFD_HTTP_MAX_WS_MESSAGE_BYTES",
    "LFD_HTTP_MAX_WS_QUEUE",
    "LFD_HTTP_MAX_WS_MALFORMED",
    "LFD_HTTP_AUTH_FAILURES_PER_MINUTE",
    "LFD_HTTP_TRUSTED_PROXY_CIDRS",
];

pub(super) fn service_environment(path_env: &str) -> Vec<(String, String)> {
    let mut values = vec![("PATH".to_string(), path_env.to_string())];
    for name in SERVICE_ENV_KEYS {
        let Ok(value) = std::env::var(name) else {
            continue;
        };
        let trimmed = value.trim();
        if trimmed.is_empty() {
            continue;
        }
        values.push((name.to_string(), trimmed.to_string()));
    }
    values
}

pub(super) fn contains_sensitive_service_env(values: &[(String, String)]) -> bool {
    values.iter().any(|(name, _)| {
        let name = name.to_ascii_uppercase();
        name.contains("TOKEN") || name.contains("SECRET") || name.contains("KEY")
    })
}

pub(super) fn write_service_file(path: &Path, content: &str, sensitive: bool) -> Result<()> {
    std::fs::write(path, content)?;
    if sensitive {
        let mut permissions = std::fs::metadata(path)?.permissions();
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            permissions.set_mode(0o600);
        }
        std::fs::set_permissions(path, permissions)?;
    }
    Ok(())
}

pub fn install(force: bool, no_interactive: bool) -> Result<(), Box<dyn std::error::Error>> {
    let config = LfdConfig::load()?;
    dispatch(&config, force, Action::Install)?;
    onboarding::run_install_onboarding(no_interactive)?;
    Ok(())
}

pub fn uninstall() -> Result<(), Box<dyn std::error::Error>> {
    let config = LfdConfig::load()?;
    dispatch(&config, false, Action::Uninstall)
}

pub fn start(force: bool) -> Result<(), Box<dyn std::error::Error>> {
    let config = LfdConfig::load()?;
    dispatch(&config, force, Action::Start)
}

pub fn stop() -> Result<(), Box<dyn std::error::Error>> {
    let config = LfdConfig::load()?;
    dispatch(&config, false, Action::Stop)
}

pub fn status() -> Result<(), Box<dyn std::error::Error>> {
    let config = LfdConfig::load()?;
    dispatch(&config, false, Action::Status)
}

enum Action {
    Install,
    Uninstall,
    Start,
    Stop,
    Status,
}

fn dispatch(
    config: &LfdConfig,
    force: bool,
    action: Action,
) -> Result<(), Box<dyn std::error::Error>> {
    match config.service_manager {
        ServiceManager::Launchd => {
            #[cfg(target_os = "macos")]
            {
                match action {
                    Action::Install => macos::install(config, force),
                    Action::Uninstall => macos::uninstall(config),
                    Action::Start => macos::start(config, force),
                    Action::Stop => macos::stop(config),
                    Action::Status => macos::status(config),
                }
                .map_err(Into::into)
            }

            #[cfg(not(target_os = "macos"))]
            {
                Err(format!(
                    "mode resolved service_manager={} on this OS; use a mode compatible with Linux",
                    config.service_manager.as_str()
                )
                .into())
            }
        }
        ServiceManager::Systemd => {
            #[cfg(target_os = "linux")]
            {
                match action {
                    Action::Install => linux::install(config, force),
                    Action::Uninstall => linux::uninstall(config),
                    Action::Start => linux::start(config, force),
                    Action::Stop => linux::stop(config),
                    Action::Status => linux::status(config),
                }
                .map_err(Into::into)
            }

            #[cfg(not(target_os = "linux"))]
            {
                Err(format!(
                    "mode resolved service_manager={} on this OS; use a mode compatible with macOS",
                    config.service_manager.as_str()
                )
                .into())
            }
        }
    }
}