use anyhow::{anyhow, Result};
use clap::Subcommand;
use console::style;
use std::fs;
use crate::git;
use crate::paths;
use crate::template::{self, Template};
#[derive(Subcommand)]
pub enum TemplateCmd {
List,
Add {
url: String,
#[arg(long)]
name: Option<String>,
},
Sync,
Remove { name: String },
}
pub fn run(cmd: TemplateCmd) -> Result<()> {
match cmd {
TemplateCmd::List => list(),
TemplateCmd::Add { url, name } => add(url, name),
TemplateCmd::Sync => sync(),
TemplateCmd::Remove { name } => remove(name),
}
}
fn list() -> Result<()> {
let templates = Template::list_all()?;
if templates.is_empty() {
println!("{}", style("No templates installed.").yellow());
println!();
println!(
" Add one with: {}",
style("devist template add <git-url>").cyan()
);
return Ok(());
}
let root = paths::templates_dir()?;
println!("{}", style("Installed templates").bold());
println!();
for t in &templates {
let m = &t.manifest.meta;
let version = if m.version.is_empty() {
"-".to_string()
} else {
m.version.clone()
};
let source = t
.path
.strip_prefix(&root)
.ok()
.and_then(|rel| rel.components().next())
.map(|c| c.as_os_str().to_string_lossy().to_string())
.unwrap_or_else(|| "?".to_string());
println!(
" {} {} {}",
style(format!("{:20}", m.name)).cyan(),
style(format!("{:8}", version)).dim(),
style(format!("from {}", source)).dim()
);
if !m.description.is_empty() {
println!(" {}", style(&m.description).dim());
}
}
println!();
Ok(())
}
fn add(url: String, name_override: Option<String>) -> Result<()> {
let name = name_override.unwrap_or_else(|| template::name_from_url(&url));
let target = paths::templates_dir()?.join(&name);
if target.exists() {
return Err(anyhow!(
"Template directory already exists: {} (use `template remove {}` first or `template sync`)",
target.display(),
name
));
}
println!(
" {} {} {}",
style("[FETCH]").cyan(),
style(&url).dim(),
style(format!("→ {}", target.display())).dim()
);
git::clone(&url, &target)?;
let direct = target.join("devist.toml").exists();
if direct {
let t = Template::load(&target)?;
println!(
" {} {} {}",
style("[OK]").green(),
style(&t.manifest.meta.name).cyan(),
style(format!("({})", target.display())).dim()
);
} else {
let mut count = 0;
for entry in fs::read_dir(&target)? {
let entry = entry?;
if entry.path().is_dir() && entry.path().join("devist.toml").exists() {
count += 1;
}
}
if count == 0 {
println!(
" {} no devist.toml found at root or in subdirectories",
style("[WARN]").yellow()
);
} else {
println!(
" {} found {} template(s) in {}",
style("[OK]").green(),
count,
style(target.display()).dim()
);
}
}
Ok(())
}
fn sync() -> Result<()> {
let root = paths::templates_dir()?;
if !root.exists() {
println!(
"{}",
style("No templates directory yet. Run `devist setup` first.").yellow()
);
return Ok(());
}
let mut any = false;
for entry in fs::read_dir(&root)? {
let entry = entry?;
let path = entry.path();
if !path.is_dir() {
continue;
}
if !path.join(".git").exists() {
continue;
}
any = true;
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("?");
match git::pull(&path) {
Ok(_) => println!(" {} {}", style("[PULL]").green(), name),
Err(e) => println!(" {} {} {}", style("[FAIL]").red(), name, style(e).dim()),
}
}
if !any {
println!(
"{}",
style("Nothing to sync. No git-managed templates found.").yellow()
);
}
Ok(())
}
fn remove(name: String) -> Result<()> {
let target = paths::templates_dir()?.join(&name);
if !target.exists() {
return Err(anyhow!("Template not found: {}", name));
}
fs::remove_dir_all(&target)?;
println!(" {} {}", style("[REMOVED]").green(), name);
Ok(())
}