rok-cli 0.3.9

Developer CLI for rok-based Axum applications
//! Project template registry for `rok new`.

pub mod api;
pub mod htmx;
pub mod microservice;
pub mod minimal;
pub mod saas;

use std::path::Path;

// ── Driver enums ──────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DbDriver {
    #[default]
    Postgres,
    Mysql,
    Sqlite,
}

impl DbDriver {
    pub fn sqlx_feature(&self) -> &'static str {
        match self {
            Self::Postgres => "postgres",
            Self::Mysql => "mysql",
            Self::Sqlite => "sqlite",
        }
    }

    pub fn example_url(&self) -> &'static str {
        match self {
            Self::Postgres => "postgres://postgres:postgres@localhost/app",
            Self::Mysql => "mysql://root:root@localhost/app",
            Self::Sqlite => "sqlite://db.sqlite3",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AuthDriver {
    #[default]
    Jwt,
    Session,
    Social,
    MagicLink,
    None,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CacheDriver {
    #[default]
    Memory,
    Redis,
    None,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum QueueDriver {
    #[default]
    None,
    Postgres,
    Redis,
}

// ── WizardConfig ──────────────────────────────────────────────────────────────

/// Choices made by the interactive `rok new` wizard.
pub struct WizardConfig {
    pub template: Template,
    pub db: DbDriver,
    pub auth: AuthDriver,
    pub cache: CacheDriver,
    pub queue: QueueDriver,
    pub include_examples: bool,
}

// ── Template enum ─────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Template {
    Api,
    Saas,
    Htmx,
    Microservice,
    Minimal,
}

impl Template {
    pub fn from_str(s: &str) -> anyhow::Result<Self> {
        match s {
            "api" => Ok(Self::Api),
            "saas" => Ok(Self::Saas),
            "htmx" => Ok(Self::Htmx),
            "microservice" => Ok(Self::Microservice),
            "minimal" => Ok(Self::Minimal),
            _ => anyhow::bail!(
                "Unknown template '{s}'. Valid options: api, saas, htmx, microservice, minimal"
            ),
        }
    }

    pub fn label(&self) -> &'static str {
        match self {
            Self::Api => "api",
            Self::Saas => "saas",
            Self::Htmx => "htmx",
            Self::Microservice => "microservice",
            Self::Minimal => "minimal",
        }
    }

    /// Whether this template creates its own root directory.
    /// Git-based templates (Api) clone into a new directory, so `rok new`
    /// should skip creating the empty root directory beforehand.
    pub fn handles_directory_creation(&self) -> bool {
        matches!(self, Self::Api)
    }

    pub fn generate(
        &self,
        project_name: &str,
        root: &Path,
        config: &WizardConfig,
    ) -> anyhow::Result<()> {
        match self {
            Self::Api => api::generate(project_name, root, config),
            Self::Saas => saas::generate(project_name, root, config),
            Self::Htmx => htmx::generate(project_name, root, config),
            Self::Microservice => microservice::generate(project_name, root, config),
            Self::Minimal => minimal::generate(project_name, root, config),
        }
    }
}

// ── Interactive wizard ────────────────────────────────────────────────────────

/// Run the full interactive wizard and return the user's choices.
pub fn prompt_wizard() -> anyhow::Result<WizardConfig> {
    use dialoguer::{Confirm, Select};

    let template_items = &[
        "api          REST API with JWT auth + CRUD ready",
        "saas         Multi-tenant SaaS (magic link, billing hooks)",
        "htmx         Full-stack with Htmx + Minijinja templates",
        "microservice Minimal service with health endpoint + Docker",
        "minimal      Bare skeleton (axum + sqlx only)",
    ];
    let tmpl_sel = Select::new()
        .with_prompt("Project template")
        .items(template_items)
        .default(0)
        .interact()?;
    let template = match tmpl_sel {
        0 => Template::Api,
        1 => Template::Saas,
        2 => Template::Htmx,
        3 => Template::Microservice,
        _ => Template::Minimal,
    };

    let db_items = &["PostgreSQL", "MySQL", "SQLite"];
    let db_sel = Select::new()
        .with_prompt("Database")
        .items(db_items)
        .default(0)
        .interact()?;
    let db = match db_sel {
        1 => DbDriver::Mysql,
        2 => DbDriver::Sqlite,
        _ => DbDriver::Postgres,
    };

    let auth_items = &["JWT", "Session", "Social (OAuth)", "Magic Link", "None"];
    let auth_sel = Select::new()
        .with_prompt("Auth driver")
        .items(auth_items)
        .default(0)
        .interact()?;
    let auth = match auth_sel {
        1 => AuthDriver::Session,
        2 => AuthDriver::Social,
        3 => AuthDriver::MagicLink,
        4 => AuthDriver::None,
        _ => AuthDriver::Jwt,
    };

    let cache_items = &["Memory (in-process)", "Redis", "None"];
    let cache_sel = Select::new()
        .with_prompt("Cache driver")
        .items(cache_items)
        .default(0)
        .interact()?;
    let cache = match cache_sel {
        1 => CacheDriver::Redis,
        2 => CacheDriver::None,
        _ => CacheDriver::Memory,
    };

    let queue_items = &["None", "PostgreSQL", "Redis"];
    let queue_sel = Select::new()
        .with_prompt("Queue driver")
        .items(queue_items)
        .default(0)
        .interact()?;
    let queue = match queue_sel {
        1 => QueueDriver::Postgres,
        2 => QueueDriver::Redis,
        _ => QueueDriver::None,
    };

    let include_examples = Confirm::new()
        .with_prompt("Include example files?")
        .default(true)
        .interact()?;

    Ok(WizardConfig {
        template,
        db,
        auth,
        cache,
        queue,
        include_examples,
    })
}