Skip to main content

packc/
runtime.rs

1#![forbid(unsafe_code)]
2
3use std::fs;
4use std::path::{Path, PathBuf};
5
6use anyhow::{Context, Result};
7use greentic_config::{ConfigLayer, ConfigResolver, ResolvedConfig};
8use greentic_types::ConnectionKind;
9use std::sync::Arc;
10
11pub struct RuntimeState {
12    pub resolved: ResolvedConfig,
13}
14
15pub type RuntimeContext = Arc<RuntimeState>;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum NetworkPolicy {
19    Online,
20    Offline,
21}
22
23impl RuntimeState {
24    pub fn cache_dir(&self) -> PathBuf {
25        self.resolved.config.paths.cache_dir.clone()
26    }
27
28    pub fn network_policy(&self) -> NetworkPolicy {
29        if matches!(
30            self.resolved.config.environment.connection,
31            Some(ConnectionKind::Offline)
32        ) {
33            NetworkPolicy::Offline
34        } else {
35            NetworkPolicy::Online
36        }
37    }
38
39    pub fn require_online(&self, action: &str) -> Result<()> {
40        match self.network_policy() {
41            NetworkPolicy::Online => Ok(()),
42            NetworkPolicy::Offline => Err(anyhow::anyhow!(
43                "network operation blocked in offline mode: {}",
44                action
45            )),
46        }
47    }
48
49    pub fn warnings(&self) -> &[String] {
50        &self.resolved.warnings
51    }
52}
53
54pub fn resolve_runtime(
55    project_root: Option<&Path>,
56    cli_cache_dir: Option<&Path>,
57    cli_offline: bool,
58    cli_override: Option<&Path>,
59) -> Result<RuntimeContext> {
60    let mut resolver = ConfigResolver::new();
61    if let Some(root) = project_root {
62        resolver = resolver.with_project_root(root.to_path_buf());
63    }
64
65    if let Some(path) = cli_override {
66        let layer = load_cli_override_layer(path)?;
67        resolver = resolver.with_cli_overrides(layer);
68    }
69
70    let mut resolved = resolver.load()?;
71
72    if cli_offline {
73        resolved.config.environment.connection = Some(ConnectionKind::Offline);
74        resolved
75            .warnings
76            .push("offline forced by CLI --offline flag".to_string());
77    }
78
79    if let Some(cache_dir) = cli_cache_dir {
80        resolved.config.paths.cache_dir = cache_dir.to_path_buf();
81    }
82
83    Ok(Arc::new(RuntimeState { resolved }))
84}
85
86fn load_cli_override_layer(path: &Path) -> Result<ConfigLayer> {
87    let contents = fs::read_to_string(path)
88        .with_context(|| format!("failed to read config override {}", path.display()))?;
89    let ext = path
90        .extension()
91        .and_then(|e| e.to_str())
92        .unwrap_or_default();
93    let layer = if ext.eq_ignore_ascii_case("json") {
94        serde_json::from_str(&contents)
95            .with_context(|| format!("{} is not valid JSON", path.display()))?
96    } else {
97        toml::from_str(&contents)
98            .with_context(|| format!("{} is not valid TOML", path.display()))?
99    };
100    Ok(layer)
101}