use std::path::{Component, Path};
use anyhow::{Context, Result};
use crate::templates;
pub fn execute(name: &str, template: Option<&str>) -> Result<()> {
validate_project_name(name)?;
let template_name = template.unwrap_or("general");
let project_dir = Path::new(name);
if project_dir.exists() {
anyhow::bail!("Directory '{}' already exists", name);
}
println!(
"Creating project '{}' with template '{}'...",
name, template_name
);
let resolved = templates::resolve(template_name)?;
for (rel_path, content) in &resolved.files {
if rel_path == "template.toml" {
continue;
}
let dest = project_dir.join(rel_path);
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&dest, content)
.with_context(|| format!("Failed to write {}", dest.display()))?;
}
let project_toml = format!(
r#"[documento]
titulo = "{name}"
autor = "Author"
template = "{template_name}"
[compilacion]
entry = "main.tex"
bibliografia = "bib/references.bib"
"#
);
std::fs::write(project_dir.join("project.toml"), project_toml)?;
std::fs::create_dir_all(project_dir.join("assets/images"))?;
println!(" ◇ Project '{}' created successfully", name);
println!();
println!(" cd {}", name);
println!(" texforge build");
Ok(())
}
pub(crate) fn validate_project_name(name: &str) -> Result<()> {
if name.is_empty() {
anyhow::bail!("Project name cannot be empty");
}
let path = Path::new(name);
for component in path.components() {
match component {
Component::ParentDir => {
anyhow::bail!("Project name cannot contain '..' (path traversal)");
}
Component::RootDir | Component::Prefix(_) => {
anyhow::bail!("Project name cannot be an absolute path");
}
_ => {}
}
}
if name.contains('/') || name.contains('\\') {
anyhow::bail!("Project name cannot contain path separators");
}
if name.contains(' ') {
anyhow::bail!("Project name cannot contain spaces — use hyphens instead (e.g. 'mi-tesis')");
}
let invalid_chars = ['@', '#', '$', '!', '&', '|', ';', '`', '"', '\'', '*', '?'];
if let Some(c) = name.chars().find(|c| invalid_chars.contains(c)) {
anyhow::bail!("Project name contains invalid character: '{}'", c);
}
if name.trim().is_empty() {
anyhow::bail!("Project name cannot be only whitespace");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_name_is_error() {
assert!(validate_project_name("").is_err());
}
#[test]
fn name_with_spaces_is_error() {
assert!(validate_project_name("my project").is_err());
}
#[test]
fn name_with_dotdot_is_error() {
assert!(validate_project_name("../evil").is_err());
}
#[test]
fn name_with_slash_is_error() {
assert!(validate_project_name("a/b").is_err());
}
#[test]
fn valid_name_is_ok() {
assert!(validate_project_name("mi-tesis").is_ok());
}
}