use anyhow::{anyhow, Result};
use clap::Subcommand;
use console::style;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use walkdir::WalkDir;
use crate::commands::init;
use crate::registry::Registry;
use crate::render;
use crate::state::State;
use crate::template::Template;
#[derive(Subcommand)]
pub enum ProjectCmd {
List,
Forget { name: String },
Sync {
name: String,
#[arg(long = "var", value_name = "KEY=VALUE")]
vars: Vec<String>,
#[arg(long)]
dry_run: bool,
},
}
pub fn run(cmd: ProjectCmd) -> Result<()> {
match cmd {
ProjectCmd::List => list(),
ProjectCmd::Forget { name } => forget(name),
ProjectCmd::Sync {
name,
vars,
dry_run,
} => sync(name, vars, dry_run),
}
}
fn list() -> Result<()> {
let reg = Registry::load()?;
let state = State::load()?;
let active = state.active_project.as_deref();
if reg.projects.is_empty() {
println!("{}", style("No projects registered.").yellow());
println!();
println!(
" Create one with: {}",
style("devist init <name> --template=<t>").cyan()
);
return Ok(());
}
println!("{}", style("Registered projects").bold());
println!();
for p in ®.projects {
let active_marker = if Some(p.name.as_str()) == active {
style("● ").green().to_string()
} else {
" ".to_string()
};
println!(
"{}{} {}",
active_marker,
style(format!("{:20}", p.name)).cyan(),
style(format!("[{}]", p.template)).dim()
);
println!(" {}", style(p.path.display()).dim());
println!(" {}", style(format!("created {}", p.created_at)).dim());
}
println!();
if active.is_some() {
println!(" {} = active backend", style("●").green());
}
Ok(())
}
fn forget(name: String) -> Result<()> {
let mut reg = Registry::load()?;
if !reg.remove(&name) {
return Err(anyhow!("Project not found in registry: {}", name));
}
reg.save()?;
let mut state = State::load()?;
if state.active_project.as_deref() == Some(&name) {
state.set_active(None);
state.save()?;
}
println!(" {} {}", style("[FORGOTTEN]").green(), name);
println!(" {}", style("(project files on disk are untouched)").dim());
Ok(())
}
fn sync(name: String, var_overrides: Vec<String>, dry_run: bool) -> Result<()> {
let reg = Registry::load()?;
let project = reg
.find(&name)
.ok_or_else(|| anyhow!("Project not found in registry: {}", name))?
.clone();
if !project.path.exists() {
return Err(anyhow!(
"Project directory missing: {}",
project.path.display()
));
}
let templates = Template::list_all()?;
let tpl = templates
.iter()
.find(|t| t.manifest.meta.name == project.template)
.ok_or_else(|| {
anyhow!(
"Template `{}` not installed. Run `devist template add ...` first.",
project.template
)
})?;
let vars = init::build_vars(tpl, &project.name, &var_overrides)?;
println!("{}", style("devist project sync").bold());
println!();
println!(
" {} {} {}",
style("[PROJ]").cyan(),
style(&project.name).bold(),
style(format!("[{}]", project.template)).dim()
);
println!(
" {} {}",
style("[TPL]").cyan(),
style(tpl.path.display()).dim()
);
if dry_run {
println!(
" {} dry-run — no files will be written",
style("[DRY]").yellow()
);
}
println!();
let counts = walk_and_add(&tpl.path, &project.path, &vars, dry_run)?;
println!();
println!(
"{}",
style(format!(
"Done. {} added, {} skipped (already exist).",
counts.added, counts.skipped
))
.green()
);
Ok(())
}
struct SyncCounts {
added: usize,
skipped: usize,
}
fn walk_and_add(
src: &Path,
dst: &Path,
vars: &HashMap<String, String>,
dry_run: bool,
) -> Result<SyncCounts> {
let mut added = 0usize;
let mut skipped = 0usize;
for entry in WalkDir::new(src).min_depth(1) {
let entry = entry?;
let path = entry.path();
let rel = path.strip_prefix(src)?;
if init::should_skip(rel) {
continue;
}
if !entry.file_type().is_file() {
continue;
}
let is_template = path.extension().and_then(|e| e.to_str()) == Some("tmpl");
let dst_rel = if is_template {
let mut p = rel.to_path_buf();
p.set_extension("");
p
} else {
rel.to_path_buf()
};
let dst_path = dst.join(&dst_rel);
if dst_path.exists() {
skipped += 1;
continue;
}
if dry_run {
println!(" {} {}", style("[+]").green(), dst_rel.display());
} else if is_template {
let body = fs::read_to_string(path)?;
let rendered_body = render::render(&body, vars)?;
if let Some(parent) = dst_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&dst_path, rendered_body)?;
println!(" {} {}", style("[+]").green(), dst_rel.display());
} else {
if let Some(parent) = dst_path.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(path, &dst_path)?;
println!(" {} {}", style("[+]").green(), dst_rel.display());
}
added += 1;
}
Ok(SyncCounts { added, skipped })
}