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}