use platform_config::ConfigLoader; use serde::{Deserialize, Serialize}; use std::path::PathBuf; /// Inner settings — matches the `ncl_sync` key in the NCL export. #[derive(Debug, Serialize, Deserialize)] pub struct NclSyncSettings { #[serde(default)] pub cache_dir: Option, #[serde(default = "default_idle_timeout")] pub idle_timeout_secs: u64, #[serde(default = "default_sync_poll_ms")] pub sync_poll_interval_ms: u64, #[serde(default = "default_concurrency")] pub warm_concurrency: usize, #[serde(default)] pub extra_import_paths: Vec, /// Additional directories to warm alongside the primary workspace. /// Useful for `$PROVISIONING/extensions/` and other shared NCL trees that live /// outside the active workspace but are frequently read by commands. /// Default: empty. Env `NCL_SYNC_EXTRA_WARM` (colon-separated) or config value. /// `~` is expanded. #[serde(default)] pub extra_warm_paths: Vec, /// Filename suffixes that identify non-exportable NCL files (schemas, contracts, lib files). /// Files matching these suffixes are skipped during warm-up and watcher events. /// Default: `["-schema.ncl", "-defaults.ncl", "-constraints.ncl"]` #[serde(default = "default_skip_patterns")] pub skip_patterns: Vec, /// Directory basenames that indicate non-exportable NCL files. /// Files in any parent directory whose basename matches are skipped. /// Default: `["schemas", "defaults", "constraints"]` #[serde(default = "default_skip_dirs")] pub skip_dirs: Vec, /// NATS-driven event subscriber (opt-in, requires `nats` Cargo feature). #[serde(default)] pub nats: NclSyncNatsSettings, } /// NATS subscription settings for event-driven cache invalidation. #[derive(Debug, Default, Serialize, Deserialize)] pub struct NclSyncNatsSettings { /// When true, connect to NATS and subscribe to `provisioning.workspace.ncl.*`. #[serde(default)] pub enabled: bool, /// NATS URL. Empty → uses platform-nats default (`nats://127.0.0.1:4222`). #[serde(default)] pub url: String, } impl Default for NclSyncSettings { fn default() -> Self { Self { cache_dir: None, idle_timeout_secs: default_idle_timeout(), sync_poll_interval_ms: default_sync_poll_ms(), warm_concurrency: default_concurrency(), extra_import_paths: Vec::new(), extra_warm_paths: Vec::new(), skip_patterns: default_skip_patterns(), skip_dirs: default_skip_dirs(), nats: NclSyncNatsSettings::default(), } } } /// Root config struct — outer key `ncl_sync` mirrors the NCL file shape. #[derive(Debug, Default, Serialize, Deserialize)] pub struct NclSyncConfig { #[serde(default)] pub ncl_sync: NclSyncSettings, } impl NclSyncConfig { pub fn load_or_default() -> Self { Self::load().unwrap_or_default() } pub fn settings(&self) -> &NclSyncSettings { &self.ncl_sync } /// Resolve cache dir. Priority: /// 1. Explicit `cache_dir` in config file /// 2. Workspace-local (`/.ncl-cache/`) if workspace provided /// 3. Global fallback (`~/.cache/provisioning/config-cache/`) pub fn resolved_cache_dir(&self, workspace: Option<&std::path::Path>) -> PathBuf { if let Some(dir) = &self.ncl_sync.cache_dir { return PathBuf::from(dir); } crate::manifest::resolve_cache_dir(workspace) } } impl ConfigLoader for NclSyncConfig { fn service_name() -> &'static str { "ncl-sync" } fn collect_env_overrides() -> serde_json::Value { let mut inner = serde_json::Map::new(); if let Ok(v) = std::env::var("NCL_SYNC_DIR") { inner.insert("cache_dir".into(), serde_json::Value::String(v)); } if let Ok(v) = std::env::var("NCL_SYNC_IDLE_TIMEOUT") { if let Ok(n) = v.parse::() { inner.insert("idle_timeout_secs".into(), serde_json::Value::Number(n.into())); } } if let Ok(v) = std::env::var("NCL_SYNC_CONCURRENCY") { if let Ok(n) = v.parse::() { inner.insert("warm_concurrency".into(), serde_json::Value::Number(n.into())); } } // When no env overrides are set, return Null to skip the merge path entirely. // Returning {"ncl_sync": {}} triggers a Nickel merge conflict with the contract- // annotated record in the NCL config file. if inner.is_empty() { return serde_json::Value::Null; } let mut root = serde_json::Map::new(); root.insert("ncl_sync".into(), serde_json::Value::Object(inner)); serde_json::Value::Object(root) } } fn default_idle_timeout() -> u64 { 600 } fn default_sync_poll_ms() -> u64 { 500 } fn default_concurrency() -> usize { 4 } fn default_skip_patterns() -> Vec { vec![ "-schema.ncl".to_string(), "-defaults.ncl".to_string(), "-constraints.ncl".to_string(), ] } fn default_skip_dirs() -> Vec { vec!["schemas".to_string(), "defaults".to_string(), "constraints".to_string()] }