lmrc-cli 0.3.16

CLI tool for scaffolding LMRC Stack infrastructure projects
Documentation
pub mod apps;
pub mod documentation;
pub mod gitlab_ci;
pub mod pipeline;
pub mod workspace;

use colored::Colorize;
use lmrc_config_validator::LmrcConfig;
use std::fs;
use std::path::{Path, PathBuf};

use crate::error::Result;

pub struct ProjectGenerator {
    config: LmrcConfig,
    project_path: PathBuf,
}

impl ProjectGenerator {
    pub fn new(config: LmrcConfig, project_path: PathBuf) -> Self {
        Self {
            config,
            project_path,
        }
    }

    pub fn project_path(&self) -> &Path {
        &self.project_path
    }

    pub async fn generate(&self) -> Result<()> {
        println!("{}", "Generating project structure...".yellow());

        // Create root directory
        fs::create_dir_all(&self.project_path)?;

        // Create directory structure
        self.create_directory_structure()?;

        // Generate configuration file
        self.generate_config_file()?;

        // Generate applications
        self.generate_applications()?;

        // Generate libs directory with placeholders
        self.generate_libs()?;

        // Generate pipeline application
        pipeline::generate_pipeline_app(&self.project_path, &self.config)?;

        // Generate workspace Cargo.toml AFTER apps and pipeline are created
        workspace::generate_workspace_toml(&self.project_path, &self.config)?;

        // Generate GitLab CI configuration
        gitlab_ci::generate_gitlab_ci(&self.project_path, &self.config)?;

        // Generate documentation
        documentation::generate_docs(&self.project_path, &self.config)?;

        // Generate Docker files
        self.generate_docker_files()?;

        // Generate .gitignore
        self.generate_gitignore()?;

        Ok(())
    }

    pub async fn generate_minimal(&self) -> Result<()> {
        println!("{}", "Generating minimal project structure...".yellow());

        // Create root directory
        fs::create_dir_all(&self.project_path)?;

        // Create directory structure
        self.create_directory_structure()?;

        // Don't generate Cargo.toml for minimal projects - it will be created when first component is added

        // Generate configuration file
        self.generate_config_file()?;

        // Generate libs directory with placeholders
        self.generate_libs()?;

        // Generate Docker files
        self.generate_docker_files()?;

        // Generate .gitignore
        self.generate_gitignore()?;

        Ok(())
    }

    fn create_directory_structure(&self) -> Result<()> {
        let dirs = vec!["apps", "libs", "docs", "docker", "infra"];

        for dir in dirs {
            let path = self.project_path.join(dir);
            fs::create_dir_all(&path)?;
            println!("  {} {}", "Created:".green(), path.display());
        }

        Ok(())
    }

    fn generate_config_file(&self) -> Result<()> {
        let config_content = self.config.to_toml_string()?;
        let config_path = self.project_path.join("lmrc.toml");
        fs::write(&config_path, config_content)?;
        println!("  {} {}", "Created:".green(), config_path.display());
        Ok(())
    }

    fn generate_applications(&self) -> Result<()> {
        // Use the new apps module to generate applications from templates
        apps::generate_applications(&self.project_path, &self.config)?;
        Ok(())
    }

    fn generate_libs(&self) -> Result<()> {
        let libs_path = self.project_path.join("libs");
        let readme = r#"# Shared Libraries

This directory contains shared libraries used across multiple applications.

Place reusable code, utilities, and shared logic here.
"#;
        fs::write(libs_path.join("README.md"), readme)?;
        println!("  {} libs/README.md", "Created:".green());
        Ok(())
    }

    fn generate_docker_files(&self) -> Result<()> {
        let docker_path = self.project_path.join("docker");

        // Generate Dockerfile template for applications
        let dockerfile = r#"# Build stage
FROM rust:1.75-slim as builder

WORKDIR /app
COPY . .

RUN cargo build --release

# Runtime stage
FROM debian:bookworm-slim

RUN apt-get update && \
    apt-get install -y ca-certificates && \
    rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY --from=builder /app/target/release/app /app/app

ENTRYPOINT ["/app/app"]
"#;

        fs::write(docker_path.join("Dockerfile"), dockerfile)?;
        println!("  {} docker/Dockerfile", "Created:".green());

        // Generate docker-compose.yml for local development with all configured services
        let docker_compose = self.generate_docker_compose()?;
        fs::write(docker_path.join("docker-compose.yml"), docker_compose)?;
        println!("  {} docker/docker-compose.yml", "Created:".green());

        Ok(())
    }

    fn generate_docker_compose(&self) -> Result<String> {
        let mut services = String::new();
        let mut volumes = String::new();

        // Add PostgreSQL if configured
        if self.config.infrastructure.postgres.is_some() {
            let pg_version = self.config
                .infrastructure
                .postgres
                .as_ref()
                .map(|p| p.version.as_str())
                .unwrap_or("16");
            let pg_db = self.config
                .infrastructure
                .postgres
                .as_ref()
                .map(|p| p.database_name.as_str())
                .unwrap_or("myapp");

            services.push_str(&format!(
                r#"  postgres:
    image: postgres:{}
    container_name: {}-postgres
    environment:
      POSTGRES_DB: {}
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - lmrc-network

"#,
                pg_version, self.config.project.name, pg_db
            ));
            volumes.push_str("  postgres_data:\n");
        }

        // Add RabbitMQ if configured
        if self.config.infrastructure.rabbitmq.is_some() {
            services.push_str(&format!(
                r#"  rabbitmq:
    image: rabbitmq:3-management
    container_name: {}-rabbitmq
    environment:
      RABBITMQ_DEFAULT_USER: guest
      RABBITMQ_DEFAULT_PASS: guest
    ports:
      - "5672:5672"   # AMQP port
      - "15672:15672" # Management UI
    volumes:
      - rabbitmq_data:/var/lib/rabbitmq
    healthcheck:
      test: ["CMD", "rabbitmq-diagnostics", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - lmrc-network

"#,
                self.config.project.name
            ));
            volumes.push_str("  rabbitmq_data:\n");
        }

        // Add Vault if configured
        if self.config.infrastructure.vault.is_some() {
            services.push_str(&format!(
                r#"  vault:
    image: hashicorp/vault:latest
    container_name: {}-vault
    environment:
      VAULT_DEV_ROOT_TOKEN_ID: root
      VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200
    ports:
      - "8200:8200"
    cap_add:
      - IPC_LOCK
    networks:
      - lmrc-network
    command: server -dev

"#,
                self.config.project.name
            ));
        }

        // Add Redis (common for caching, always include)
        services.push_str(&format!(
            r#"  redis:
    image: redis:7-alpine
    container_name: {}-redis
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - lmrc-network

"#,
            self.config.project.name
        ));
        volumes.push_str("  redis_data:\n");

        // Build complete docker-compose.yml
        let mut compose = String::from("version: '3.8'\n\nservices:\n");
        compose.push_str(&services);

        if !volumes.is_empty() {
            compose.push_str("\nvolumes:\n");
            compose.push_str(&volumes);
        }

        compose.push_str(
            r#"
networks:
  lmrc-network:
    driver: bridge
"#,
        );

        Ok(compose)
    }

    fn generate_gitignore(&self) -> Result<()> {
        let gitignore = r#"# Rust
/target/
**/*.rs.bk
*.pdb
Cargo.lock

# IDE
.idea/
.vscode/
*.swp
*.swo
*~

# OS
.DS_Store
Thumbs.db

# Environment
.env
.env.local

# Secrets
secrets/
.secrets/
*.pem
*.key

# SSH Keys (project-local)
.ssh/
*.pub
"#;

        let gitignore_path = self.project_path.join(".gitignore");
        fs::write(&gitignore_path, gitignore)?;
        println!("  {} .gitignore", "Created:".green());

        Ok(())
    }
}