cargo-bloom-web 0.1.0

A CLI tool for initializing Bloom web applications with Docker support
use std::process::{Command, Stdio};
use colored::Colorize;
use console_utils::input::select;

#[tokio::main]
async fn main() {
    let args: Vec<String> = std::env::args().collect();

    if args.len() > 1 && (
        (args.len() > 2 && args[1] == "bloom-web" && args[2] == "init") ||
        (args[1] == "init")
    ) {
        if !check_git_installed() {
            eprintln!("{} {}", "".bright_red().bold(), "Git is not installed or not found in PATH".red());
            eprintln!("{} {}", "".bright_blue().bold(), "Please install Git from https://git-scm.com/downloads".blue());
            return;
        }

        if std::path::Path::new("./bloom-app").exists() {
            if !ask_for_overwrite() {
                println!("{} {}", "".bright_blue().bold(), "Operation cancelled.".blue());
                return;
            }
            if let Err(_) = std::fs::remove_dir_all("./bloom-app") {
                eprintln!("{} {}", "".bright_red().bold(), "Failed to remove existing bloom-app directory".red());
                return;
            }
        }

        println!("{} {}", "".bright_blue().bold(), "Initializing new Bloom app...".blue());

        let repo_url = "https://github.com/matusmesko/bloom-initializr.git";
        let status = Command::new("git")
            .args(&["clone", repo_url, "./bloom-app"])
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .status()
            .expect("Failed to execute git");

        if status.success() {

            if ask_for_docker_setup() {
                setup_docker().await;
            }
            println!("{} {}", "".bright_green().bold(), "Successfully initialized new Bloom app in ./bloom-app".green());
        } else {
            eprintln!("{} {}", "".bright_red().bold(), "Failed to clone repository".red());
            eprintln!("{} {}", "".bright_blue().bold(), "Please check your internet connection or try again".blue());
        }
    } else if args.len() > 1 && (
        (args.len() > 2 && args[1] == "bloom-web" && args[2] == "version") ||
        (args[1] == "version")
    ) {
        println!("{} {} {}", "".bright_blue().bold(), "Latest version: ".yellow(), get_latest_version_from_api().await.unwrap().dimmed());
    } else {
        eprintln!("{} {} {}", "Usage:".bright_white().bold(), "cargo bloom-web init".bright_cyan(), "- Initialize a new Bloom app".dimmed());
        eprintln!("{} {} {}", "Usage:".bright_white().bold(), "cargo bloom-web version".bright_cyan(), "- Show latest version of bloom-web".dimmed());
    }
}

fn check_git_installed() -> bool {
    Command::new("git")
        .args(&["--version"])
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .map(|status| status.success())
        .unwrap_or(false)
}

async fn get_latest_version_from_api() -> Result<String, Box<dyn std::error::Error>> {
    let response = reqwest::get("https://crates.io/api/v1/crates/bloom-web")
        .await?
        .json::<serde_json::Value>()
        .await?;

    let version = response["crate"]["max_version"]
        .as_str()
        .unwrap_or("0.1.5")
        .to_string();

    Ok(version)
}


fn ask_for_docker_setup() -> bool {
    let options = ["No", "Yes"];
    let selection = select("Would you like to set up Docker for this project?", &options);
    match selection {
        0 => return false,
        1 => return true,
        _ => return false,
    }
}

fn ask_for_database_setup() -> bool {
    let options = ["No", "Yes"];
    let selection = select("Would you like to add a MySQL database to Docker?", &options);
    match selection {
        0 => return false,
        1 => return true,
        _ => return false,
    }
}

fn ask_for_overwrite() -> bool {
    println!("{} {}", "".bright_yellow().bold(), "Directory './bloom-app' already exists.".yellow());

    let options = ["No", "Yes"];
    let selection = select("Do you want to overwrite it?", &options);
    match selection {
        0 => return false,
        1 => return true,
        _ => return false,
    }
}

async fn setup_docker() {
    println!("{} {}", "".bright_blue().bold(), "Setting up Docker configuration...".blue());

    let cargo_content = std::fs::read_to_string("./bloom-app/Cargo.toml").unwrap_or_default();
    let project_name = cargo_content
        .lines()
        .find(|line| line.starts_with("name = "))
        .and_then(|line| line.split('"').nth(1))
        .unwrap_or("bloom-app");

    let include_database = ask_for_database_setup();

    if include_database {
        update_config_database_url();
    }

    let dockerfile_content = format!(r#"FROM rustlang/rust:nightly as builder

WORKDIR /usr/src/app
COPY Cargo.toml Cargo.lock ./
COPY src ./src
COPY config.toml ./

RUN cargo build --release

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 /usr/src/app/target/release/{} /app/{}
COPY --from=builder /usr/src/app/config.toml /app/

EXPOSE 8080

CMD ["./{}"]
"#, project_name, project_name, project_name);

    let dockerfile_dev_content = r#"FROM rustlang/rust:nightly

# Install useful development tools
RUN apt-get update && apt-get install -y \
    ca-certificates \
    git \
    curl \
    vim \
    && rm -rf /var/lib/apt/lists/*

# Install cargo-watch for hot reloading
RUN cargo install cargo-watch

WORKDIR /app

# Copy dependency files first for better layer caching
COPY Cargo.toml Cargo.lock ./

# Build dependencies (this layer will be cached)
RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build && rm -rf src

# Copy source code and config
COPY . .

EXPOSE 8080

# Use cargo-watch for hot reloading in development
CMD ["cargo", "watch", "-x", "run"]
"#;

    let mut docker_compose_content = format!(r#"services:
  # Production service
  bloom-app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - RUST_LOG=info
      - BLOOM_ENV=production{}
    restart: unless-stopped

  # Development service
  bloom-app-dev:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "8081:8080"
    environment:
      - RUST_LOG=debug
      - BLOOM_ENV=development{}
    volumes:
      - ./src:/app/src:ro
      - ./Cargo.toml:/app/Cargo.toml:ro
      - ./Cargo.lock:/app/Cargo.lock:ro
    restart: unless-stopped
"#,
        if include_database { "\n      - DATABASE_URL=mysql://bloom_user:bloom_password@mysql:3306/bloom_db" } else { "" },
        if include_database { "\n      - DATABASE_URL=mysql://bloom_user:bloom_password@mysql:3306/bloom_db" } else { "" }
    );

    if include_database {
        docker_compose_content.push_str(r#"
  mysql:
    image: mysql:8.0
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: root_password
      MYSQL_DATABASE: bloom_db
      MYSQL_USER: bloom_user
      MYSQL_PASSWORD: bloom_password
    volumes:
      - mysql_data:/var/lib/mysql
    restart: unless-stopped

volumes:
  mysql_data:
"#);
    }

    let dockerignore_content = r#"target/
.git
.gitignore
README.md
.env
.env.local
.idea/
.vscode/
*.log
.DS_Store
Dockerfile
Dockerfile.dev
docker-compose.yml
"#;

    let env_dev_content = if include_database {
        r#"# Development environment variables
RUST_LOG=debug
BLOOM_ENV=development
DATABASE_URL=mysql://bloom_user:bloom_password@localhost:3306/bloom_db
"#
    } else {
        r#"# Development environment variables
RUST_LOG=debug
BLOOM_ENV=development
"#
    };

    match std::fs::write("./bloom-app/Dockerfile", dockerfile_content) {
        Ok(_) => println!("{} {}", "".bright_green().bold(), "Created Dockerfile (production)".green()),
        Err(_) => eprintln!("{} {}", "".bright_red().bold(), "Failed to create Dockerfile".red()),
    }

    match std::fs::write("./bloom-app/Dockerfile.dev", dockerfile_dev_content) {
        Ok(_) => println!("{} {}", "".bright_green().bold(), "Created Dockerfile.dev (development)".green()),
        Err(_) => eprintln!("{} {}", "".bright_red().bold(), "Failed to create Dockerfile.dev".red()),
    }

    match std::fs::write("./bloom-app/docker-compose.yml", docker_compose_content) {
        Ok(_) => println!("{} {}", "".bright_green().bold(), "Created docker-compose.yml".green()),
        Err(_) => eprintln!("{} {}", "".bright_red().bold(), "Failed to create docker-compose.yml".red()),
    }

    match std::fs::write("./bloom-app/.dockerignore", dockerignore_content) {
        Ok(_) => println!("{} {}", "".bright_green().bold(), "Created .dockerignore".green()),
        Err(_) => eprintln!("{} {}", "".bright_red().bold(), "Failed to create .dockerignore".red()),
    }

    match std::fs::write("./bloom-app/.env.development", env_dev_content) {
        Ok(_) => println!("{} {}", "".bright_green().bold(), "Created .env.development".green()),
        Err(_) => eprintln!("{} {}", "".bright_red().bold(), "Failed to create .env.development".red()),
    }

    print_docker_info(include_database);
}

fn update_config_database_url() {
    let config_path = "./bloom-app/config.toml";
    match std::fs::read_to_string(config_path) {
        Ok(content) => {
            let updated_content = content.replace(
                r#"database_url = "mysql://user:pass@host:3306/dbname""#,
                r#"database_url = "mysql://bloom_user:bloom_password@localhost:3306/bloom_db""#
            );

            match std::fs::write(config_path, updated_content) {
                Ok(_) => println!("{} {}", "".bright_green().bold(), "Updated config.toml with database URL".green()),
                Err(_) => eprintln!("{} {}", "".bright_red().bold(), "Failed to update config.toml".red()),
            }
        }
        Err(_) => eprintln!("{} {}", "".bright_red().bold(), "Failed to read config.toml".red()),
    }
}

fn print_docker_info(include_database: bool) {
    println!();
    println!("{}", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".bright_cyan());
    println!("{} {}", "🐳".bright_blue().bold(), "DOCKER SETUP COMPLETE".bright_white().bold());
    println!("{}", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".bright_cyan());
    println!();

    println!("{} {}", "📦".bright_blue().bold(), "AVAILABLE SERVICES:".bright_white().bold());
    println!("  {} {}", "".bright_cyan(), "bloom-app (Production) - Optimized build".dimmed());
    println!("  {} {}", "".bright_cyan(), "bloom-app-dev (Development) - Hot reloading".dimmed());
    if include_database {
        println!("  {} {}", "".bright_cyan(), "mysql - MySQL 8.0 Database".dimmed());
    }
    println!();

    println!("{} {}", "🚀".bright_blue().bold(), "QUICK START COMMANDS:".bright_white().bold());
    if include_database {
        println!("  {} {}", "Development with DB:".bright_yellow().bold(), "docker compose up bloom-app-dev mysql --build".bright_cyan());
        println!("  {} {}", "Development only:".bright_yellow().bold(), "docker compose up bloom-app-dev --build".bright_cyan());
        println!("  {} {}", "Production with DB:".bright_yellow().bold(), "docker compose up bloom-app mysql --build".bright_cyan());
        println!("  {} {}", "Production only:".bright_yellow().bold(), "docker compose up bloom-app --build".bright_cyan());
    } else {
        println!("  {} {}", "Development:".bright_yellow().bold(), "docker compose up bloom-app-dev --build".bright_cyan());
        println!("  {} {}", "Production:".bright_yellow().bold(), "docker compose up bloom-app --build".bright_cyan());
    }
    println!();

    println!("{} {}", "🌐".bright_blue().bold(), "ACCESS URLS:".bright_white().bold());
    println!("  {} {}", "Production:".bright_yellow().bold(), "http://localhost:8080".bright_cyan());
    println!("  {} {}", "Development:".bright_yellow().bold(), "http://localhost:8081".bright_cyan());
    println!();

    if include_database {
        println!("{} {}", "🗄️".bright_blue().bold(), "DATABASE CONNECTION:".bright_white().bold());
        println!("  {} {}", "Host:".bright_yellow().bold(), "localhost:3306".white());
        println!("  {} {}", "Database:".bright_yellow().bold(), "bloom_db".white());
        println!("  {} {}", "Username:".bright_yellow().bold(), "bloom_user".white());
        println!("  {} {}", "Password:".bright_yellow().bold(), "bloom_password".white());
        println!("  {} {}", "Root Password:".bright_yellow().bold(), "root_password".white());
        println!("  {} {}", "Connection URL:".bright_yellow().bold(), "mysql://bloom_user:bloom_password@localhost:3306/bloom_db".white());
        println!();
    }

    println!("{} {}", "🔧".bright_blue().bold(), "USEFUL COMMANDS:".bright_white().bold());
    println!("  {} {}", "Stop all:".bright_yellow().bold(), "docker compose down".bright_cyan());
    println!("  {} {}", "View logs:".bright_yellow().bold(), "docker compose logs -f [service-name]".bright_cyan());
    println!("  {} {}", "Rebuild:".bright_yellow().bold(), "docker compose up --build [service-name]".bright_cyan());
    if include_database {
        println!("  {} {}", "Database shell:".bright_yellow().bold(), "docker compose exec mysql mysql -u bloom_user -p bloom_db".bright_cyan());
        println!("  {} {}", "Reset database:".bright_yellow().bold(), "docker compose down -v && docker compose up mysql".bright_cyan());
    }

    println!();

}