use anyhow::{Result, Context};
use colored::Colorize;
use dialoguer::{Confirm, Input, Select};
use indicatif::{ProgressBar, ProgressStyle};
use std::fs;
use std::path::Path;
use std::time::Duration;
use crate::utils::{print_success, print_error, print_info};
pub async fn handle_init(name: &str, template: &str) -> Result<()> {
println!("{}", "Revoke Framework Project Initialization".bold());
println!("{}", "=".repeat(50));
if Path::new(name).exists() {
print_error(&format!("Directory '{}' already exists", name));
return Err(anyhow::anyhow!("Directory already exists"));
}
let confirm = Confirm::new()
.with_prompt(format!("Create new Revoke project '{}'?", name))
.default(true)
.interact()?;
if !confirm {
print_info("Project creation cancelled");
return Ok(());
}
let template = if template == "basic" {
let templates = vec!["basic", "microservice", "gateway", "full-stack"];
let selection = Select::new()
.with_prompt("Select project template")
.items(&templates)
.default(0)
.interact()?;
templates[selection]
} else {
template
};
let package_name = Input::<String>::new()
.with_prompt("Package name")
.default(name.to_string())
.interact()?;
let description = Input::<String>::new()
.with_prompt("Description")
.default("A Revoke framework microservices project".to_string())
.interact()?;
let author = Input::<String>::new()
.with_prompt("Author")
.default(get_git_author().unwrap_or_else(|_| "".to_string()))
.interact()?;
let pb = ProgressBar::new(5);
pb.set_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{bar:40.cyan/blue}] {msg}")
.unwrap()
.progress_chars("#>-")
);
pb.set_message("Creating project structure...");
create_project_structure(name, template)?;
pb.inc(1);
tokio::time::sleep(Duration::from_millis(300)).await;
pb.set_message("Writing configuration files...");
write_cargo_toml(name, &package_name, &description, &author, template)?;
write_gitignore(name)?;
pb.inc(1);
tokio::time::sleep(Duration::from_millis(300)).await;
pb.set_message("Creating source files...");
write_source_files(name, template)?;
pb.inc(1);
tokio::time::sleep(Duration::from_millis(300)).await;
pb.set_message("Writing Docker configuration...");
write_docker_files(name, template)?;
pb.inc(1);
tokio::time::sleep(Duration::from_millis(300)).await;
pb.set_message("Creating README...");
write_readme(name, &description, template)?;
pb.inc(1);
pb.finish_with_message("Project created successfully!");
println!();
print_success(&format!("Project '{}' created successfully!", name));
println!("\n{}", "Next steps:".bold());
println!(" 1. cd {}", name);
println!(" 2. cargo build");
println!(" 3. docker-compose up -d");
println!(" 4. revoke dev --all");
println!("\n{}", "Useful commands:".bold());
println!(" {} - List all services", "revoke service list".cyan());
println!(" {} - Check system health", "revoke health check".cyan());
println!(" {} - View configuration", "revoke config list".cyan());
Ok(())
}
fn create_project_structure(name: &str, template: &str) -> Result<()> {
fs::create_dir(name)?;
fs::create_dir_all(format!("{}/src", name))?;
match template {
"microservice" => {
fs::create_dir_all(format!("{}/src/handlers", name))?;
fs::create_dir_all(format!("{}/src/models", name))?;
fs::create_dir_all(format!("{}/src/services", name))?;
}
"gateway" => {
fs::create_dir_all(format!("{}/src/routes", name))?;
fs::create_dir_all(format!("{}/src/middleware", name))?;
}
"full-stack" => {
fs::create_dir_all(format!("{}/services/user-service/src", name))?;
fs::create_dir_all(format!("{}/services/order-service/src", name))?;
fs::create_dir_all(format!("{}/gateway/src", name))?;
fs::create_dir_all(format!("{}/frontend", name))?;
}
_ => {}
}
Ok(())
}
fn write_cargo_toml(
project_name: &str,
package_name: &str,
description: &str,
author: &str,
template: &str,
) -> Result<()> {
let content = match template {
"microservice" => format!(
r#"[package]
name = "{}"
version = "0.1.0"
edition = "2024"
authors = ["{}"]
description = "{}"
[dependencies]
revoke-core = {{ git = "https://github.com/revoke-framework/revoke" }}
revoke-registry = {{ git = "https://github.com/revoke-framework/revoke", features = ["consul"] }}
revoke-config = {{ git = "https://github.com/revoke-framework/revoke", features = ["consul"] }}
revoke-trace = {{ git = "https://github.com/revoke-framework/revoke" }}
revoke-resilience = {{ git = "https://github.com/revoke-framework/revoke" }}
tokio = {{ version = "1", features = ["full"] }}
axum = "0.8"
serde = {{ version = "1", features = ["derive"] }}
serde_json = "1"
tracing = "0.1"
tracing-subscriber = {{ version = "0.3", features = ["env-filter"] }}
anyhow = "1.0"
"#,
package_name, author, description
),
"gateway" => format!(
r#"[package]
name = "{}"
version = "0.1.0"
edition = "2024"
authors = ["{}"]
description = "{}"
[dependencies]
revoke-gateway = {{ git = "https://github.com/revoke-framework/revoke" }}
revoke-registry = {{ git = "https://github.com/revoke-framework/revoke", features = ["consul"] }}
revoke-trace = {{ git = "https://github.com/revoke-framework/revoke" }}
tokio = {{ version = "1", features = ["full"] }}
serde = {{ version = "1", features = ["derive"] }}
serde_json = "1"
tracing = "0.1"
tracing-subscriber = {{ version = "0.3", features = ["env-filter"] }}
anyhow = "1.0"
"#,
package_name, author, description
),
"full-stack" => format!(
r#"[workspace]
members = [
"services/user-service",
"services/order-service",
"gateway",
]
resolver = "2"
[workspace.package]
version = "0.1.0"
edition = "2024"
authors = ["{}"]
[workspace.dependencies]
revoke-core = {{ git = "https://github.com/revoke-framework/revoke" }}
revoke-registry = {{ git = "https://github.com/revoke-framework/revoke", features = ["consul"] }}
revoke-config = {{ git = "https://github.com/revoke-framework/revoke", features = ["consul"] }}
revoke-trace = {{ git = "https://github.com/revoke-framework/revoke" }}
revoke-gateway = {{ git = "https://github.com/revoke-framework/revoke" }}
revoke-resilience = {{ git = "https://github.com/revoke-framework/revoke" }}
tokio = {{ version = "1", features = ["full"] }}
serde = {{ version = "1", features = ["derive"] }}
serde_json = "1"
tracing = "0.1"
anyhow = "1.0"
"#,
author
),
_ => format!(
r#"[package]
name = "{}"
version = "0.1.0"
edition = "2024"
authors = ["{}"]
description = "{}"
[dependencies]
tokio = {{ version = "1", features = ["full"] }}
"#,
package_name, author, description
),
};
let path = if template == "full-stack" {
format!("{}/Cargo.toml", project_name)
} else {
format!("{}/Cargo.toml", project_name)
};
fs::write(path, content)?;
Ok(())
}
fn write_source_files(name: &str, template: &str) -> Result<()> {
match template {
"microservice" => {
let main_rs = r#"use anyhow::Result;
use axum::{Router, routing::get};
use revoke_registry::{ServiceRegistry, ServiceInfo};
use revoke_config::ConfigProvider;
use revoke_trace::init_tracer;
use std::net::SocketAddr;
use tracing::info;
#[tokio::main]
async fn main() -> Result<()> {
// Initialize tracing
init_tracer("my-service").await?;
// Initialize service registry
let registry = revoke_registry::consul::ConsulRegistry::new("http://localhost:8500").await?;
// Register service
let service_info = ServiceInfo {
id: format!("my-service-{}", uuid::Uuid::new_v4()),
name: "my-service".to_string(),
address: "127.0.0.1".to_string(),
port: 8080,
tags: vec!["api".to_string()],
status: revoke_core::ServiceStatus::Healthy,
metadata: Default::default(),
};
registry.register(service_info).await?;
// Create router
let app = Router::new()
.route("/health", get(health_check));
// Start server
let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
info!("Starting server on {}", addr);
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app).await?;
Ok(())
}
async fn health_check() -> &'static str {
"OK"
}
"#;
fs::write(format!("{}/src/main.rs", name), main_rs)?;
}
_ => {
let main_rs = r#"fn main() {
println!("Hello, Revoke!");
}
"#;
fs::write(format!("{}/src/main.rs", name), main_rs)?;
}
}
Ok(())
}
fn write_docker_files(name: &str, template: &str) -> Result<()> {
let dockerfile = r#"FROM rust:1.84 as builder
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
COPY src ./src
RUN cargo build --release
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/app /usr/local/bin/app
EXPOSE 8080
CMD ["app"]
"#;
let docker_compose = match template {
"full-stack" => r#"version: '3.8'
services:
consul:
image: consul:latest
ports:
- "8500:8500"
command: agent -dev -ui -client=0.0.0.0
gateway:
build: ./gateway
ports:
- "8080:8080"
environment:
- CONSUL_ADDR=http://consul:8500
depends_on:
- consul
user-service:
build: ./services/user-service
environment:
- CONSUL_ADDR=http://consul:8500
depends_on:
- consul
order-service:
build: ./services/order-service
environment:
- CONSUL_ADDR=http://consul:8500
depends_on:
- consul
"#,
_ => r#"version: '3.8'
services:
consul:
image: consul:latest
ports:
- "8500:8500"
command: agent -dev -ui -client=0.0.0.0
app:
build: .
ports:
- "8080:8080"
environment:
- CONSUL_ADDR=http://consul:8500
depends_on:
- consul
"#,
};
fs::write(format!("{}/Dockerfile", name), dockerfile)?;
fs::write(format!("{}/docker-compose.yml", name), docker_compose)?;
Ok(())
}
fn write_gitignore(name: &str) -> Result<()> {
let content = r#"# Rust
target/
Cargo.lock
**/*.rs.bk
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Environment
.env
.env.local
"#;
fs::write(format!("{}/.gitignore", name), content)?;
Ok(())
}
fn write_readme(name: &str, description: &str, template: &str) -> Result<()> {
let content = format!(
r#"# {}
{}
## Quick Start
```bash
# Start infrastructure
docker-compose up -d
# Build project
cargo build
# Run tests
cargo test
# Start development
revoke dev --all
```
## Project Structure
This is a {} project built with the Revoke framework.
## Configuration
Configuration is managed through environment variables and Consul.
## Documentation
For more information, see the [Revoke Framework documentation](https://github.com/revoke-framework/revoke).
"#,
name,
description,
template
);
fs::write(format!("{}/README.md", name), content)?;
Ok(())
}
fn get_git_author() -> Result<String> {
let output = std::process::Command::new("git")
.args(&["config", "user.name"])
.output()?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
} else {
Err(anyhow::anyhow!("Failed to get git author"))
}
}