Skip to main content

life_cli/
template.rs

1//! Agent template loading, validation, and listing.
2
3use std::collections::HashMap;
4
5use anyhow::{Context, Result};
6use serde::{Deserialize, Serialize};
7use tabled::{Table, Tabled};
8
9use crate::cli::{TemplateArgs, TemplateCommand};
10
11/// A service within an agent template.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ServiceDef {
14    /// Container image (e.g., ghcr.io/broomva/arcan:latest).
15    pub image: String,
16    /// Port the service listens on.
17    pub port: u16,
18    /// Whether to expose a public domain.
19    #[serde(default = "default_true")]
20    pub public: bool,
21    /// Health check endpoint path.
22    #[serde(default = "default_health_path")]
23    pub health_path: String,
24    /// Environment variables specific to this service.
25    #[serde(default)]
26    pub env: HashMap<String, String>,
27    /// Startup command override (uses container default if empty).
28    #[serde(default)]
29    pub command: Option<String>,
30    /// Volume mount path for persistent data.
31    #[serde(default)]
32    pub volume: Option<String>,
33}
34
35fn default_true() -> bool {
36    true
37}
38
39fn default_health_path() -> String {
40    "/health".to_string()
41}
42
43/// An agent template — a pre-configured stack of Life services.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct AgentTemplate {
46    /// Template metadata.
47    pub meta: TemplateMeta,
48    /// Services composing this agent.
49    pub services: HashMap<String, ServiceDef>,
50    /// Shared environment variables applied to all services.
51    #[serde(default)]
52    pub shared_env: HashMap<String, String>,
53    /// Autonomic scaling configuration.
54    #[serde(default)]
55    pub scaling: ScalingConfig,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct TemplateMeta {
60    /// Human-readable name.
61    pub name: String,
62    /// Short description.
63    pub description: String,
64    /// Use case this template is designed for.
65    pub use_case: String,
66    /// Version.
67    #[serde(default = "default_version")]
68    pub version: String,
69}
70
71fn default_version() -> String {
72    "0.1.0".to_string()
73}
74
75/// Autonomic-driven scaling configuration.
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct ScalingConfig {
78    /// Minimum replicas.
79    #[serde(default = "default_min_replicas")]
80    pub min_replicas: u32,
81    /// Maximum replicas.
82    #[serde(default = "default_max_replicas")]
83    pub max_replicas: u32,
84    /// Economic mode that triggers scale-down.
85    #[serde(default = "default_scale_down_mode")]
86    pub scale_down_mode: String,
87    /// Economic mode that triggers scale-up.
88    #[serde(default = "default_scale_up_mode")]
89    pub scale_up_mode: String,
90}
91
92fn default_min_replicas() -> u32 {
93    1
94}
95fn default_max_replicas() -> u32 {
96    3
97}
98fn default_scale_down_mode() -> String {
99    "conserving".to_string()
100}
101fn default_scale_up_mode() -> String {
102    "sovereign".to_string()
103}
104
105impl Default for ScalingConfig {
106    fn default() -> Self {
107        Self {
108            min_replicas: default_min_replicas(),
109            max_replicas: default_max_replicas(),
110            scale_down_mode: default_scale_down_mode(),
111            scale_up_mode: default_scale_up_mode(),
112        }
113    }
114}
115
116// ── Built-in templates ──────────────────────────────────────────────────────
117
118const CODING_AGENT_TOML: &str = include_str!("../templates/coding-agent.toml");
119const DATA_AGENT_TOML: &str = include_str!("../templates/data-agent.toml");
120const SUPPORT_AGENT_TOML: &str = include_str!("../templates/support-agent.toml");
121
122/// Load a template by name — checks built-in templates first, then custom path.
123pub fn load_template(name: &str, custom_path: Option<&str>) -> Result<AgentTemplate> {
124    // Check custom path first
125    if let Some(path) = custom_path {
126        let content = std::fs::read_to_string(path)
127            .with_context(|| format!("failed to read template file: {path}"))?;
128        return toml::from_str(&content)
129            .with_context(|| format!("failed to parse template file: {path}"));
130    }
131
132    // Check built-in templates
133    let toml_str = match name {
134        "coding-agent" => CODING_AGENT_TOML,
135        "data-agent" => DATA_AGENT_TOML,
136        "support-agent" => SUPPORT_AGENT_TOML,
137        _ => {
138            // Try loading from ~/.life/templates/{name}.toml
139            let home = dirs::home_dir().context("cannot determine home directory")?;
140            let path = home
141                .join(".life")
142                .join("templates")
143                .join(format!("{name}.toml"));
144            if path.exists() {
145                let content = std::fs::read_to_string(&path)
146                    .with_context(|| format!("failed to read {}", path.display()))?;
147                return toml::from_str(&content)
148                    .with_context(|| format!("failed to parse {}", path.display()));
149            }
150            anyhow::bail!(
151                "unknown template '{name}'. Available: coding-agent, data-agent, support-agent.\n\
152                 Custom templates: place TOML files in ~/.life/templates/ or use --template-path."
153            );
154        }
155    };
156
157    toml::from_str(toml_str).with_context(|| format!("failed to parse built-in template: {name}"))
158}
159
160/// List all available templates (built-in + custom).
161pub fn list_templates() -> Vec<AgentTemplate> {
162    let mut templates = vec![];
163
164    for toml_str in [CODING_AGENT_TOML, DATA_AGENT_TOML, SUPPORT_AGENT_TOML] {
165        if let Ok(t) = toml::from_str::<AgentTemplate>(toml_str) {
166            templates.push(t);
167        }
168    }
169
170    // Scan ~/.life/templates/ for custom templates
171    if let Some(home) = dirs::home_dir() {
172        let custom_dir = home.join(".life").join("templates");
173        if custom_dir.is_dir() {
174            if let Ok(entries) = std::fs::read_dir(custom_dir) {
175                for entry in entries.flatten() {
176                    let path = entry.path();
177                    if path.extension().is_some_and(|e| e == "toml") {
178                        if let Ok(content) = std::fs::read_to_string(&path) {
179                            if let Ok(t) = toml::from_str::<AgentTemplate>(&content) {
180                                templates.push(t);
181                            }
182                        }
183                    }
184                }
185            }
186        }
187    }
188
189    templates
190}
191
192// ── CLI handler ─────────────────────────────────────────────────────────────
193
194#[derive(Tabled)]
195struct TemplateRow {
196    #[tabled(rename = "Name")]
197    name: String,
198    #[tabled(rename = "Description")]
199    description: String,
200    #[tabled(rename = "Services")]
201    services: String,
202    #[tabled(rename = "Use Case")]
203    use_case: String,
204}
205
206pub fn run(args: TemplateArgs) -> Result<()> {
207    match args.command {
208        TemplateCommand::List => {
209            let templates = list_templates();
210            if templates.is_empty() {
211                println!("No templates found.");
212                return Ok(());
213            }
214
215            let rows: Vec<TemplateRow> = templates
216                .iter()
217                .map(|t| {
218                    let svc_names: Vec<&str> = t.services.keys().map(String::as_str).collect();
219                    TemplateRow {
220                        name: t.meta.name.clone(),
221                        description: t.meta.description.clone(),
222                        services: svc_names.join(", "),
223                        use_case: t.meta.use_case.clone(),
224                    }
225                })
226                .collect();
227
228            println!("{}", Table::new(rows));
229            Ok(())
230        }
231        TemplateCommand::Show { name } => {
232            let template = load_template(&name, None)?;
233            println!("{}", toml::to_string_pretty(&template)?);
234            Ok(())
235        }
236    }
237}