use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use colored::*;
use dialoguer::{Confirm, Input};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use walkdir::WalkDir;
const REPO_URL: &str = "https://github.com/bmartel/django-lightning.git";
#[derive(Parser)]
#[command(
name = "create-django-bolt",
author,
version,
about = "Ultra-fast CLI binary to scaffold production-ready Django-Bolt applications"
)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
#[arg(value_name = "PROJECT_NAME")]
name: Option<String>,
#[arg(short, long)]
path: Option<PathBuf>,
#[arg(short, long)]
template: Option<String>,
#[arg(long, default_value_t = false)]
github_actions: bool,
#[arg(long, default_value_t = false)]
no_rust: bool,
}
#[derive(Subcommand)]
enum Commands {
New {
name: Option<String>,
#[arg(short, long)]
path: Option<PathBuf>,
#[arg(short, long)]
template: Option<String>,
#[arg(long, default_value_t = false)]
github_actions: bool,
#[arg(long, default_value_t = false)]
no_rust: bool,
},
}
fn sanitize_names(input: &str) -> (String, String) {
let clean = input.trim().to_lowercase();
let slug = clean.replace(['_', ' '], "-");
let snake = clean.replace(['-', ' '], "_");
(slug, snake)
}
fn is_dir_empty(path: &Path) -> bool {
if !path.exists() {
return true;
}
match fs::read_dir(path) {
Ok(mut entries) => entries.next().is_none(),
Err(_) => false,
}
}
fn copy_and_transform_dir(
src_dir: &Path,
dest_dir: &Path,
slug_name: &str,
snake_name: &str,
include_ci: bool,
include_rust: bool,
) -> Result<()> {
let mut ignored_dirs = vec![
".git", "git", ".venv", "venv", "__pycache__", ".pytest_cache", ".ruff_cache",
"staticfiles", "scratch", "target", "cli", ".worktrees",
];
if !include_rust {
ignored_dirs.push("rust_core");
}
let ignored_files = ["db.sqlite3", "db.sqlite3-journal", ".DS_Store"];
for entry in WalkDir::new(src_dir).into_iter().filter_map(|e| e.ok()) {
let path = entry.path();
let rel_path = path.strip_prefix(src_dir)?;
if rel_path.components().any(|c| {
let name = c.as_os_str().to_string_lossy();
ignored_dirs.contains(&name.as_ref())
}) {
continue;
}
if rel_path == Path::new(".github/workflows/release.yml") {
continue;
}
if !include_ci && rel_path.starts_with(".github") {
continue;
}
if !include_rust && rel_path == Path::new("app/routes/rust_demo.py") {
continue;
}
let target_path = dest_dir.join(rel_path);
if path.is_dir() {
fs::create_dir_all(&target_path)?;
} else if path.is_file() {
let file_name = path.file_name().unwrap_or_default().to_string_lossy();
if ignored_files.contains(&file_name.as_ref()) || file_name.ends_with(".pyc") {
continue;
}
if let Ok(content) = fs::read_to_string(path) {
let mut transformed = content
.replace("django-lightning-mcp", &format!("{}-mcp", slug_name))
.replace("django-lightning", slug_name)
.replace("django_lightning", snake_name)
.replace("Django Lightning", &slug_name.replace('-', " "));
if file_name == "justfile" {
transformed = transformed.replace(
"\n# Build the Rust CLI tool (create-django-bolt)\nbuild-cli:\n cargo build --manifest-path cli/Cargo.toml --release\n",
"",
);
if !include_rust {
let rust_tasks = "# Compile Rust core in debug mode for local development\nrust-dev:\n uv run maturin develop\n\n# Compile Rust core in release mode for maximum production performance\nrust-build:\n uv run maturin build --release --manifest-path rust_core/Cargo.toml --out target/wheels && uv pip install target/wheels/*.whl\n\n# Run unit tests for Rust native core crate\nrust-test:\n cargo test --manifest-path rust_core/Cargo.toml\n";
transformed = transformed.replace(rust_tasks, "");
}
}
if !include_rust && file_name == "api.py" {
transformed = transformed.replace("from app.routes.rust_demo import register_rust_routes\n", "");
transformed = transformed.replace("register_rust_routes(api)\n", "");
}
if !include_rust && file_name == "pyproject.toml" {
let maturin_build = "[tool.maturin]\nmanifest-path = \"rust_core/Cargo.toml\"\npython-packages = [\"app\"]\nmodule-name = \"app.rust_core\"\n";
transformed = transformed.replace(maturin_build, "");
transformed = transformed.replace(" \"maturin>=1.5.0\",\n", "");
}
fs::write(&target_path, transformed)?;
} else {
fs::copy(path, &target_path)?;
}
}
}
Ok(())
}
fn transform_in_place(
dest_dir: &Path,
slug_name: &str,
snake_name: &str,
include_ci: bool,
include_rust: bool,
) -> Result<()> {
let mut ignored_dirs = vec![
".git", "git", ".venv", "venv", "__pycache__", ".pytest_cache", ".ruff_cache",
"staticfiles", "scratch", "target", "cli",
];
if !include_rust {
ignored_dirs.push("rust_core");
let _ = fs::remove_dir_all(dest_dir.join("rust_core"));
let _ = fs::remove_file(dest_dir.join("app/routes/rust_demo.py"));
}
let ignored_files = ["db.sqlite3", "db.sqlite3-journal", ".DS_Store"];
let _ = fs::remove_file(dest_dir.join(".github/workflows/release.yml"));
if !include_ci {
let _ = fs::remove_dir_all(dest_dir.join(".github"));
}
for entry in WalkDir::new(dest_dir).into_iter().filter_map(|e| e.ok()) {
let path = entry.path();
let rel_path = path.strip_prefix(dest_dir)?;
if rel_path.components().any(|c| {
let name = c.as_os_str().to_string_lossy();
ignored_dirs.contains(&name.as_ref())
}) {
continue;
}
if path.is_file() {
let file_name = path.file_name().unwrap_or_default().to_string_lossy();
if ignored_files.contains(&file_name.as_ref()) || file_name.ends_with(".pyc") {
continue;
}
if let Ok(content) = fs::read_to_string(&path) {
let mut transformed = content
.replace("django-lightning-mcp", &format!("{}-mcp", slug_name))
.replace("django-lightning", slug_name)
.replace("django_lightning", snake_name)
.replace("Django Lightning", &slug_name.replace('-', " "));
if file_name == "justfile" {
transformed = transformed.replace(
"\n# Build the Rust CLI tool (create-django-bolt)\nbuild-cli:\n cargo build --manifest-path cli/Cargo.toml --release\n",
"",
);
if !include_rust {
let rust_tasks = "# Compile Rust core in debug mode for local development\nrust-dev:\n uv run maturin develop\n\n# Compile Rust core in release mode for maximum production performance\nrust-build:\n uv run maturin develop --release\n\n# Run unit tests for Rust native core crate\nrust-test:\n cargo test --manifest-path rust_core/Cargo.toml\n";
transformed = transformed.replace(rust_tasks, "");
}
}
if !include_rust && file_name == "api.py" {
transformed = transformed.replace("from app.routes.rust_demo import register_rust_routes\n", "");
transformed = transformed.replace("register_rust_routes(api)\n", "");
}
if !include_rust && file_name == "pyproject.toml" {
let maturin_build = "[build-system]\nrequires = [\"maturin>=1.5,<2.0\"]\nbuild-backend = \"maturin\"\n\n[tool.maturin]\nmanifest-path = \"rust_core/Cargo.toml\"\npython-packages = [\"app\"]\nmodule-name = \"app.rust_core\"\n";
transformed = transformed.replace(maturin_build, "");
transformed = transformed.replace(" \"maturin>=1.5.0\",\n", "");
}
let _ = fs::write(&path, transformed);
}
}
}
Ok(())
}
fn download_template_from_github(dest_dir: &Path) -> Result<()> {
println!(" {}", "Downloading template from GitHub (bmartel/django-lightning)...".dimmed());
if !dest_dir.exists() {
let status = Command::new("git")
.args(["clone", "--depth", "1", REPO_URL, &dest_dir.to_string_lossy()])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.context("Failed to execute git clone")?;
if !status.success() {
anyhow::bail!("Failed to clone template repository from '{}'", REPO_URL);
}
} else {
let status = Command::new("git")
.args(["clone", "--depth", "1", REPO_URL, "."])
.current_dir(dest_dir)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.context("Failed to execute git clone")?;
if !status.success() {
anyhow::bail!("Failed to clone template repository into existing directory '{}'", dest_dir.display());
}
}
Ok(())
}
fn initialize_git(dest_dir: &Path) {
let _ = fs::remove_dir_all(dest_dir.join(".git"));
if Command::new("git")
.args(["init"])
.current_dir(dest_dir)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
{
println!(" {}", "✓ Initialized Git repository".green());
}
}
fn setup_uv_env(dest_dir: &Path) {
println!("\n{}", "âš™ Setting up Python environment with uv...".cyan());
let venv_status = Command::new("uv")
.arg("venv")
.current_dir(dest_dir)
.status();
if venv_status.map(|s| s.success()).unwrap_or(false) {
println!(" {}", "✓ Created virtual environment (.venv)".green());
let install_status = Command::new("uv")
.args(["pip", "install", "-e", ".[dev]"])
.current_dir(dest_dir)
.status();
if install_status.map(|s| s.success()).unwrap_or(false) {
println!(" {}", "✓ Installed dependencies with uv".green());
}
} else {
println!(" {}", "! 'uv' not found or failed. Skipping virtual environment creation.".yellow());
}
}
fn run_generator(
name_opt: Option<String>,
path_opt: Option<PathBuf>,
template_opt: Option<String>,
github_actions_flag: bool,
no_rust_flag: bool,
) -> Result<()> {
println!("{}", "âš¡ create-django-bolt".bold().cyan());
println!("{}", " High-Performance Django-Bolt Project Generator\n".dimmed());
let raw_name = match name_opt {
Some(n) => n,
None => Input::<String>::new()
.with_prompt("Project name")
.default("my-bolt-app".into())
.interact_text()?,
};
let (slug_name, snake_name) = sanitize_names(&raw_name);
let is_tty = dialoguer::console::user_attended_stderr() || dialoguer::console::Term::stdout().is_term();
let include_ci = if github_actions_flag {
true
} else if is_tty {
Confirm::new()
.with_prompt("Include preconfigured GitHub Actions CI/CD workflows?")
.default(true)
.interact_opt()?
.unwrap_or(true)
} else {
true
};
let include_rust = if no_rust_flag {
false
} else if is_tty {
Confirm::new()
.with_prompt("Include native Rust extension core (rust_core)?")
.default(true)
.interact_opt()?
.unwrap_or(true)
} else {
true
};
let current_dir = std::env::current_dir()?;
let dest_dir = match path_opt {
Some(p) => p,
None => current_dir.join(&slug_name),
};
if dest_dir.exists() && !is_dir_empty(&dest_dir) {
anyhow::bail!(
"Destination directory '{}' already exists and is not empty!\nPlease specify a different name or path (e.g. create-django-bolt new {} -p /path/to/dir).",
dest_dir.display(),
slug_name
);
}
println!("🚀 Creating Django-Bolt project '{}' in '{}'...", slug_name.bold().green(), dest_dir.display());
if let Some(template_path) = template_opt {
let src_path = PathBuf::from(template_path);
if !src_path.exists() {
anyhow::bail!("Template path '{}' does not exist!", src_path.display());
}
fs::create_dir_all(&dest_dir)?;
copy_and_transform_dir(&src_path, &dest_dir, &slug_name, &snake_name, include_ci, include_rust)?;
} else if current_dir.join("manage.py").exists() && current_dir.join("pyproject.toml").exists() {
fs::create_dir_all(&dest_dir)?;
copy_and_transform_dir(¤t_dir, &dest_dir, &slug_name, &snake_name, include_ci, include_rust)?;
} else {
download_template_from_github(&dest_dir)?;
transform_in_place(&dest_dir, &slug_name, &snake_name, include_ci, include_rust)?;
}
initialize_git(&dest_dir);
if include_ci {
println!(" {}", "✓ Added GitHub Actions CI/CD workflows (.github/workflows)".green());
}
if include_rust {
println!(" {}", "✓ Added Native Rust extension core (rust_core)".green());
} else {
println!(" {}", "ℹ Python-only project configured (rust_core excluded)".dimmed());
}
let setup_env = if is_tty {
Confirm::new()
.with_prompt("Would you like to setup virtual environment and dependencies with 'uv' now?")
.default(true)
.interact_opt()?
.unwrap_or(false)
} else {
false
};
if setup_env {
setup_uv_env(&dest_dir);
}
println!("\n{}", "✨ Project scaffolding complete!".bold().green());
println!("\nNext steps:");
let mut step = 1;
println!(" {}. cd {}", step, dest_dir.display().to_string().bold());
step += 1;
if !setup_env {
println!(" {}. uv venv", step);
step += 1;
if include_rust {
println!(" {}. uv run maturin develop", step);
step += 1;
}
println!(" {}. uv pip install -e \".[dev]\"", step);
step += 1;
}
println!(" {}. uv run manage.py migrate", step);
step += 1;
println!(" {}. uv run manage.py collectstatic --noinput", step);
step += 1;
println!(" {}. uv run manage.py runbolt --dev", step);
Ok(())
}
fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Some(Commands::New {
name,
path,
template,
github_actions,
no_rust,
}) => run_generator(name, path, template, github_actions, no_rust)?,
None => run_generator(cli.name, cli.path, cli.template, cli.github_actions, cli.no_rust)?,
}
Ok(())
}