use anyhow::{anyhow, Result};
use chrono::Local;
use console::style;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
use crate::registry::{Project, Registry};
use crate::render;
use crate::template::Template;
pub fn run(
name: String,
template_name: String,
target_dir: Option<PathBuf>,
vars_cli: Vec<String>,
) -> Result<()> {
println!("{}", style("devist init").bold());
println!();
let templates = Template::list_all()?;
let tpl = templates
.iter()
.find(|t| t.manifest.meta.name == template_name)
.ok_or_else(|| {
anyhow!(
"Template not found: {}. Run `devist template list` to see installed.",
template_name
)
})?;
println!(
" {} {} {}",
style("[TPL]").cyan(),
style(&tpl.manifest.meta.name).bold(),
style(format!("at {}", tpl.path.display())).dim()
);
let target = match target_dir {
Some(p) => p,
None => std::env::current_dir()?.join(&name),
};
if target.exists() {
return Err(anyhow!("Target already exists: {}", target.display()));
}
println!(
" {} {}",
style("[DIR]").cyan(),
style(target.display()).bold()
);
let vars = build_vars(tpl, &name, &vars_cli)?;
if !vars.is_empty() {
println!(" {} {} variables", style("[VAR]").cyan(), vars.len());
for (k, v) in &vars {
println!(" {} = {}", style(k).dim(), style(v).dim());
}
}
println!(" {} copying files...", style("[COPY]").cyan());
let counts = copy_with_render(&tpl.path, &target, &vars)?;
println!(
" {} files, {} rendered",
counts.total, counts.rendered
);
let mut reg = Registry::load()?;
reg.add(Project {
name: name.clone(),
path: target.clone(),
template: template_name.clone(),
created_at: Local::now().to_rfc3339(),
});
reg.save()?;
println!(
" {} registered as {}",
style("[REG]").green(),
style(&name).bold()
);
println!();
println!("{}", style("Done.").green());
println!();
println!(" Next:");
println!(" cd {}", target.display());
Ok(())
}
pub(crate) fn build_vars(
tpl: &Template,
project_name: &str,
cli_overrides: &[String],
) -> Result<HashMap<String, String>> {
let mut vars: HashMap<String, String> = HashMap::new();
vars.insert("project_name".to_string(), project_name.to_string());
vars.insert("folder_name".to_string(), project_name.to_string());
for (key, spec) in &tpl.manifest.variables {
vars.entry(key.clone())
.or_insert_with(|| spec.default.clone());
}
for raw in cli_overrides {
let (k, v) = raw
.split_once('=')
.ok_or_else(|| anyhow!("Invalid --var format (expected key=value): {}", raw))?;
vars.insert(k.trim().to_string(), v.trim().to_string());
}
let snapshot = vars.clone();
for (_k, v) in vars.iter_mut() {
if v.contains("{{") {
*v = render::render(v, &snapshot)?;
}
}
let mode = vars
.get("agent_mode")
.map(|s| s.trim().to_lowercase())
.unwrap_or_else(|| "devist".to_string());
match mode.as_str() {
"devist" => {
vars.insert("agent_mode".into(), "devist".into());
vars.insert("is_devist".into(), "true".into());
vars.insert("is_vibe".into(), String::new());
}
"vibe" => {
vars.insert("agent_mode".into(), "vibe".into());
vars.insert("is_devist".into(), String::new());
vars.insert("is_vibe".into(), "true".into());
}
other => {
return Err(anyhow!(
"Unknown agent_mode `{}` (expected `devist` or `vibe`)",
other
));
}
}
Ok(vars)
}
struct CopyCounts {
total: usize,
rendered: usize,
}
fn copy_with_render(src: &Path, dst: &Path, vars: &HashMap<String, String>) -> Result<CopyCounts> {
let mut total = 0;
let mut rendered = 0;
fs::create_dir_all(dst)?;
for entry in WalkDir::new(src).min_depth(1) {
let entry = entry?;
let path = entry.path();
let rel = path.strip_prefix(src)?;
if should_skip(rel) {
continue;
}
let dst_path = dst.join(rel);
if entry.file_type().is_dir() {
fs::create_dir_all(&dst_path)?;
continue;
}
if !entry.file_type().is_file() {
continue;
}
total += 1;
if path.extension().and_then(|e| e.to_str()) == Some("tmpl") {
let body = fs::read_to_string(path)?;
let rendered_body = render::render(&body, vars)?;
let final_dst = dst_path.with_extension("");
if let Some(parent) = final_dst.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&final_dst, rendered_body)?;
rendered += 1;
} else {
if let Some(parent) = dst_path.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(path, &dst_path)?;
}
}
Ok(CopyCounts { total, rendered })
}
pub(crate) fn should_skip(rel: &Path) -> bool {
let s = rel.to_string_lossy();
if s == "devist.toml" {
return true;
}
if s.contains(".git/") || s.contains(".git\\") || s == ".DS_Store" || s.ends_with("/.DS_Store")
{
return true;
}
false
}