create-lamdera-app-rs 0.1.8

A CLI tool to scaffold Lamdera applications with Tailwind CSS, authentication, i18n, and testing
Documentation
use clap::{Parser, ValueEnum};

#[derive(Debug, Clone, ValueEnum)]
pub enum PackageManager {
    Npm,
    Bun,
}

impl PackageManager {
    pub fn as_str(&self) -> &str {
        match self {
            PackageManager::Npm => "npm",
            PackageManager::Bun => "bun",
        }
    }
}

#[derive(Parser, Debug)]
#[command(
    name = "create-lamdera-app",
    version,
    about = "A CLI tool to scaffold Lamdera applications",
    long_about = "Create Lamdera App - Scaffold a full-stack Lamdera application with Tailwind CSS, authentication, i18n, dark mode, and testing support."
)]
pub struct Args {
    /// Project name
    #[arg(short, long)]
    pub name: Option<String>,

    /// Create GitHub repository (yes/no)
    #[arg(long)]
    pub github: Option<bool>,

    /// Don't create GitHub repository
    #[arg(long, conflicts_with = "github")]
    pub no_github: bool,

    /// Make GitHub repository public
    #[arg(long)]
    pub public: bool,

    /// Skip package installation
    #[arg(long)]
    pub skip_install: bool,

    /// Choose package manager
    #[arg(long, value_enum, default_value = "npm")]
    pub package_manager: PackageManager,

    /// Use Bun package manager (shorthand for --package-manager bun)
    #[arg(long)]
    pub bun: bool,

    /// Use simple boilerplate without demo features (counter/chat)
    #[arg(long)]
    pub simple: bool,

    /// Add features to existing project
    #[arg(long)]
    pub init: bool,

    /// Install pre-commit hooks
    #[arg(long)]
    pub install_precommit: bool,
}

impl Args {
    pub fn get_package_manager(&self) -> PackageManager {
        if self.bun {
            PackageManager::Bun
        } else {
            self.package_manager.clone()
        }
    }

    pub fn should_create_github(&self) -> Option<bool> {
        if self.no_github {
            Some(false)
        } else {
            self.github
        }
    }

    pub fn validate(&self) -> Result<(), String> {
        if let Some(ref name) = self.name {
            validate_project_name(name)?;
        }
        Ok(())
    }
}

pub fn validate_project_name(name: &str) -> Result<(), String> {
    if name.is_empty() {
        return Err("Project name cannot be empty".to_string());
    }

    // Check for invalid characters
    if !name
        .chars()
        .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
    {
        return Err(format!(
            "Project name '{}' contains invalid characters. Use only letters, numbers, hyphens, and underscores.",
            name
        ));
    }

    // Check if starts with number
    if name.chars().next().map_or(false, |c| c.is_numeric()) {
        return Err(format!("Project name '{}' cannot start with a number", name));
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_valid_project_names() {
        assert!(validate_project_name("my-app").is_ok());
        assert!(validate_project_name("my_app").is_ok());
        assert!(validate_project_name("MyApp").is_ok());
        assert!(validate_project_name("app123").is_ok());
    }

    #[test]
    fn test_invalid_project_names() {
        assert!(validate_project_name("").is_err());
        assert!(validate_project_name("123app").is_err());
        assert!(validate_project_name("my app").is_err());
        assert!(validate_project_name("my@app").is_err());
    }
}