use anyhow::{Context, Result};
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use crate::paths;
#[derive(Debug, Deserialize)]
pub struct TemplateMeta {
pub name: String,
#[serde(default)]
pub version: String,
#[serde(default)]
pub description: String,
#[serde(default)]
#[allow(dead_code)]
pub author: String,
#[serde(default)]
#[allow(dead_code)]
pub tags: Vec<String>,
}
#[derive(Debug, Deserialize, Default)]
pub struct VariableSpec {
#[serde(default)]
#[allow(dead_code)]
pub prompt: String,
#[serde(default)]
pub default: String,
}
#[derive(Debug, Deserialize, Default)]
pub struct StackSpec {
#[serde(rename = "type", default = "default_stack_type")]
#[allow(dead_code)]
pub kind: String,
}
fn default_stack_type() -> String {
"none".to_string()
}
#[derive(Debug, Deserialize, Default)]
pub struct CommandsSpec {
#[serde(default)]
pub install: Option<String>,
#[serde(default)]
pub dev: Option<String>,
#[serde(default)]
pub backend_start: Option<String>,
#[serde(default)]
pub backend_stop: Option<String>,
#[serde(default)]
#[allow(dead_code)]
pub test: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct TemplateManifest {
pub meta: TemplateMeta,
#[serde(default)]
pub variables: HashMap<String, VariableSpec>,
#[serde(default)]
#[allow(dead_code)]
pub stack: StackSpec,
#[serde(default)]
pub commands: CommandsSpec,
}
#[derive(Debug)]
pub struct Template {
pub manifest: TemplateManifest,
pub path: PathBuf,
}
impl Template {
pub fn load(dir: &Path) -> Result<Self> {
let manifest_path = dir.join("devist.toml");
let text = fs::read_to_string(&manifest_path).with_context(|| {
format!(
"Could not read template manifest: {}",
manifest_path.display()
)
})?;
let manifest: TemplateManifest = toml::from_str(&text)
.with_context(|| format!("Invalid devist.toml: {}", manifest_path.display()))?;
Ok(Template {
manifest,
path: dir.to_path_buf(),
})
}
pub fn list_all() -> Result<Vec<Template>> {
let root = paths::templates_dir()?;
if !root.exists() {
return Ok(Vec::new());
}
let mut found = Vec::new();
for entry in fs::read_dir(&root)? {
let entry = entry?;
let path = entry.path();
if !path.is_dir() {
continue;
}
if is_hidden(&path) {
continue;
}
if path.join("devist.toml").exists() {
try_push(&path, &mut found);
continue;
}
let inner = match fs::read_dir(&path) {
Ok(it) => it,
Err(_) => continue,
};
for sub in inner {
let sub = match sub {
Ok(e) => e,
Err(_) => continue,
};
let sub_path = sub.path();
if !sub_path.is_dir() || is_hidden(&sub_path) {
continue;
}
if sub_path.join("devist.toml").exists() {
try_push(&sub_path, &mut found);
}
}
}
found.sort_by(|a, b| a.manifest.meta.name.cmp(&b.manifest.meta.name));
Ok(found)
}
}
fn is_hidden(path: &Path) -> bool {
path.file_name()
.and_then(|n| n.to_str())
.map(|n| n.starts_with('.'))
.unwrap_or(false)
}
fn try_push(dir: &Path, out: &mut Vec<Template>) {
match Template::load(dir) {
Ok(t) => out.push(t),
Err(e) => eprintln!(" Warning: skipping {}: {}", dir.display(), e),
}
}
pub fn name_from_url(url: &str) -> String {
let trimmed = url.trim().trim_end_matches('/').trim_end_matches(".git");
let last = trimmed.rsplit(['/', ':']).next().unwrap_or(trimmed);
last.to_string()
}