greentic-pack-dev 1.1.26495471727

Greentic pack builder CLI
Documentation
#![forbid(unsafe_code)]

use std::fs;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result};
use greentic_config::{ConfigLayer, ConfigResolver, ResolvedConfig};
use greentic_types::ConnectionKind;
use std::sync::Arc;

pub struct RuntimeState {
    pub resolved: ResolvedConfig,
}

pub type RuntimeContext = Arc<RuntimeState>;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NetworkPolicy {
    Online,
    Offline,
}

impl RuntimeState {
    pub fn cache_dir(&self) -> PathBuf {
        self.resolved.config.paths.cache_dir.clone()
    }

    pub fn network_policy(&self) -> NetworkPolicy {
        if matches!(
            self.resolved.config.environment.connection,
            Some(ConnectionKind::Offline)
        ) {
            NetworkPolicy::Offline
        } else {
            NetworkPolicy::Online
        }
    }

    pub fn require_online(&self, action: &str) -> Result<()> {
        match self.network_policy() {
            NetworkPolicy::Online => Ok(()),
            NetworkPolicy::Offline => Err(anyhow::anyhow!(
                "network operation blocked in offline mode: {}",
                action
            )),
        }
    }

    pub fn warnings(&self) -> &[String] {
        &self.resolved.warnings
    }
}

pub fn resolve_runtime(
    project_root: Option<&Path>,
    cli_cache_dir: Option<&Path>,
    cli_offline: bool,
    cli_override: Option<&Path>,
) -> Result<RuntimeContext> {
    let mut resolver = ConfigResolver::new();
    if let Some(root) = project_root {
        resolver = resolver.with_project_root(root.to_path_buf());
    }

    if let Some(path) = cli_override {
        let layer = load_cli_override_layer(path)?;
        resolver = resolver.with_cli_overrides(layer);
    }

    let mut resolved = resolver.load()?;

    if cli_offline {
        resolved.config.environment.connection = Some(ConnectionKind::Offline);
        resolved
            .warnings
            .push("offline forced by CLI --offline flag".to_string());
    }

    if let Some(cache_dir) = cli_cache_dir {
        resolved.config.paths.cache_dir = cache_dir.to_path_buf();
    }

    Ok(Arc::new(RuntimeState { resolved }))
}

fn load_cli_override_layer(path: &Path) -> Result<ConfigLayer> {
    let contents = fs::read_to_string(path)
        .with_context(|| format!("failed to read config override {}", path.display()))?;
    let ext = path
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or_default();
    let layer = if ext.eq_ignore_ascii_case("json") {
        serde_json::from_str(&contents)
            .with_context(|| format!("{} is not valid JSON", path.display()))?
    } else {
        toml::from_str(&contents)
            .with_context(|| format!("{} is not valid TOML", path.display()))?
    };
    Ok(layer)
}