//! Read-only registry of workflow template recipes loaded from //! `templates/workflows/*.toml` at server boot. Slice 4. //! //! Recipes are immutable reference data — no DB row per recipe. //! Slice 2's client-side `TEMPLATE_PRESETS` is a mirror of what //! ends up here; a follow-up serves this registry over an API so //! the client can drop its inline mirror. use serde::Deserialize; use std::path::PathBuf; use std::sync::OnceLock; #[derive(Debug, Clone, Deserialize)] pub struct WorkflowRecipe { pub key: String, pub title: String, pub blurb: String, #[serde(default)] pub requires_repo: bool, #[serde(default)] pub phases: Vec, #[serde(default)] pub default_team_template: Option, } #[derive(Debug, Clone, Deserialize)] pub struct WorkflowPhase { pub kind: String, pub order_idx: i32, #[serde(default)] pub config: serde_json::Value, } static REGISTRY: OnceLock> = OnceLock::new(); fn workflows_dir() -> PathBuf { if let Ok(d) = std::env::var("CLAWMATES_WORKFLOWS_DIR") { return PathBuf::from(d); } let container = PathBuf::from("/etc/clawmates/templates/workflows"); if container.exists() { return container; } PathBuf::from("templates/workflows") } /// Load recipes from disk. Called once at boot; subsequent calls /// return the cached set. Missing/broken files log + are skipped. pub fn load() -> &'static [WorkflowRecipe] { REGISTRY.get_or_init(|| { let dir = workflows_dir(); let entries = match std::fs::read_dir(&dir) { Ok(r) => r, Err(e) => { eprintln!( "workflow_registry: dir {} not readable: {e} — no recipes", dir.display() ); return Vec::new(); } }; let mut out = Vec::new(); for entry in entries.flatten() { let path = entry.path(); if path.extension().and_then(|s| s.to_str()) != Some("toml") { continue; } match load_one(&path) { Ok(r) => { eprintln!("workflow_registry: loaded {}", r.key); out.push(r); } Err(e) => { eprintln!("workflow_registry: failed to load {}: {e}", path.display()); } } } out.sort_by(|a, b| a.key.cmp(&b.key)); out }) } fn load_one(path: &std::path::Path) -> Result { let text = std::fs::read_to_string(path).map_err(|e| format!("read {}: {e}", path.display()))?; toml::from_str::(&text).map_err(|e| format!("parse {}: {e}", path.display())) } pub fn get(key: &str) -> Option<&'static WorkflowRecipe> { load().iter().find(|r| r.key == key) }