use std::collections::HashMap;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use tabled::{Table, Tabled};
use crate::cli::{TemplateArgs, TemplateCommand};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceDef {
pub image: String,
pub port: u16,
#[serde(default = "default_true")]
pub public: bool,
#[serde(default = "default_health_path")]
pub health_path: String,
#[serde(default)]
pub env: HashMap<String, String>,
#[serde(default)]
pub command: Option<String>,
#[serde(default)]
pub volume: Option<String>,
}
fn default_true() -> bool {
true
}
fn default_health_path() -> String {
"/health".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentTemplate {
pub meta: TemplateMeta,
pub services: HashMap<String, ServiceDef>,
#[serde(default)]
pub shared_env: HashMap<String, String>,
#[serde(default)]
pub scaling: ScalingConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemplateMeta {
pub name: String,
pub description: String,
pub use_case: String,
#[serde(default = "default_version")]
pub version: String,
}
fn default_version() -> String {
"0.1.0".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScalingConfig {
#[serde(default = "default_min_replicas")]
pub min_replicas: u32,
#[serde(default = "default_max_replicas")]
pub max_replicas: u32,
#[serde(default = "default_scale_down_mode")]
pub scale_down_mode: String,
#[serde(default = "default_scale_up_mode")]
pub scale_up_mode: String,
}
fn default_min_replicas() -> u32 {
1
}
fn default_max_replicas() -> u32 {
3
}
fn default_scale_down_mode() -> String {
"conserving".to_string()
}
fn default_scale_up_mode() -> String {
"sovereign".to_string()
}
impl Default for ScalingConfig {
fn default() -> Self {
Self {
min_replicas: default_min_replicas(),
max_replicas: default_max_replicas(),
scale_down_mode: default_scale_down_mode(),
scale_up_mode: default_scale_up_mode(),
}
}
}
const CODING_AGENT_TOML: &str = include_str!("../templates/coding-agent.toml");
const DATA_AGENT_TOML: &str = include_str!("../templates/data-agent.toml");
const SUPPORT_AGENT_TOML: &str = include_str!("../templates/support-agent.toml");
pub fn load_template(name: &str, custom_path: Option<&str>) -> Result<AgentTemplate> {
if let Some(path) = custom_path {
let content = std::fs::read_to_string(path)
.with_context(|| format!("failed to read template file: {path}"))?;
return toml::from_str(&content)
.with_context(|| format!("failed to parse template file: {path}"));
}
let toml_str = match name {
"coding-agent" => CODING_AGENT_TOML,
"data-agent" => DATA_AGENT_TOML,
"support-agent" => SUPPORT_AGENT_TOML,
_ => {
let home = dirs::home_dir().context("cannot determine home directory")?;
let path = home
.join(".life")
.join("templates")
.join(format!("{name}.toml"));
if path.exists() {
let content = std::fs::read_to_string(&path)
.with_context(|| format!("failed to read {}", path.display()))?;
return toml::from_str(&content)
.with_context(|| format!("failed to parse {}", path.display()));
}
anyhow::bail!(
"unknown template '{name}'. Available: coding-agent, data-agent, support-agent.\n\
Custom templates: place TOML files in ~/.life/templates/ or use --template-path."
);
}
};
toml::from_str(toml_str).with_context(|| format!("failed to parse built-in template: {name}"))
}
pub fn list_templates() -> Vec<AgentTemplate> {
let mut templates = vec![];
for toml_str in [CODING_AGENT_TOML, DATA_AGENT_TOML, SUPPORT_AGENT_TOML] {
if let Ok(t) = toml::from_str::<AgentTemplate>(toml_str) {
templates.push(t);
}
}
if let Some(home) = dirs::home_dir() {
let custom_dir = home.join(".life").join("templates");
if custom_dir.is_dir() {
if let Ok(entries) = std::fs::read_dir(custom_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "toml") {
if let Ok(content) = std::fs::read_to_string(&path) {
if let Ok(t) = toml::from_str::<AgentTemplate>(&content) {
templates.push(t);
}
}
}
}
}
}
}
templates
}
#[derive(Tabled)]
struct TemplateRow {
#[tabled(rename = "Name")]
name: String,
#[tabled(rename = "Description")]
description: String,
#[tabled(rename = "Services")]
services: String,
#[tabled(rename = "Use Case")]
use_case: String,
}
pub fn run(args: TemplateArgs) -> Result<()> {
match args.command {
TemplateCommand::List => {
let templates = list_templates();
if templates.is_empty() {
println!("No templates found.");
return Ok(());
}
let rows: Vec<TemplateRow> = templates
.iter()
.map(|t| {
let svc_names: Vec<&str> = t.services.keys().map(String::as_str).collect();
TemplateRow {
name: t.meta.name.clone(),
description: t.meta.description.clone(),
services: svc_names.join(", "),
use_case: t.meta.use_case.clone(),
}
})
.collect();
println!("{}", Table::new(rows));
Ok(())
}
TemplateCommand::Show { name } => {
let template = load_template(&name, None)?;
println!("{}", toml::to_string_pretty(&template)?);
Ok(())
}
}
}