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!();
}