Skip to main content

rustbasic_core/cli/
mod.rs

1use std::env;
2use dotenvy::dotenv;
3use colored::*;
4use std::future::Future;
5use std::pin::Pin;
6
7pub mod scaffolding;
8pub mod database;
9pub mod monitoring;
10pub mod builder;
11pub mod utils;
12pub mod auth;
13
14pub type AsyncHook = Box<dyn Fn() -> Pin<Box<dyn Future<Output = ()>>>>;
15
16pub async fn run_cli<F, G>(migrate_fn: F, seed_fn: G) 
17where 
18    F: Fn(String) -> Pin<Box<dyn Future<Output = Result<(), String>>>>,
19    G: Fn() -> Pin<Box<dyn Future<Output = ()>>>
20{
21    let args: Vec<String> = env::args().collect();
22
23    if args.len() < 2 {
24        print_help();
25        return;
26    }
27
28    let command = &args[1];
29
30    // Delegation: Jika kita di dalam project RustBasic dan menjalankan perintah 
31    // yang butuh kompilasi lokal (seperti migrate), delegasikan ke 'cargo run'.
32    // Ini memastikan migrasi lokal yang baru dibuat bisa terbaca.
33    let commands_to_delegate = [
34        "migrate", "migrate:refresh", "migrate:back", "migrate:rollback", 
35        "db:seed", "route:list", "build"
36    ];
37
38    if env::var("RUSTBASIC_LOCAL").is_err() 
39        && std::path::Path::new("Cargo.toml").exists() 
40        && commands_to_delegate.contains(&command.as_str()) 
41    {
42        let status = std::process::Command::new("cargo")
43            .args(["run", "-q", "--bin", "rustbasic-cli", "--"])
44            .args(&args[1..])
45            .env("RUSTBASIC_LOCAL", "true")
46            .status();
47
48        match status {
49            Ok(s) => std::process::exit(s.code().unwrap_or(0)),
50            Err(_) => {
51                // Jika gagal (mungkin bukan project RustBasic yang valid), 
52                // lanjutkan eksekusi menggunakan binary ini.
53            }
54        }
55    }
56
57    // .env hanya diwajibkan untuk perintah selain 'new'
58    if command != "new" {
59        let _ = dotenv(); // Coba muat .env jika ada
60    }
61
62    match command.as_str() {
63        "-v" | "--version" | "version" => {
64            println!("{} {}", "šŸ› ļø  RustBasic CLI Version:".magenta().bold(), env!("CARGO_PKG_VERSION").cyan().bold());
65            return;
66        }
67        "serve" => {
68            println!("\n   {} {}", "šŸš€".bold(), "Menjalankan server RustBasic dengan Auto-Reload...".magenta().bold());
69            
70            // Ambil APP_URL dari .env (sudah dimuat oleh dotenv() di awal)
71            let app_url = env::var("APP_URL").unwrap_or_else(|_| "http://localhost:4000".to_string());
72            utils::wait_and_open(app_url);
73
74            let status = std::process::Command::new("cargo")
75                .args(["watch", "-c", "-q", "--no-ignore", "-i", "target", "-w", "src", "-w", ".env", "-w", "src/resources", "-x", "run"])
76                .status()
77                .expect("āŒ Gagal menjalankan cargo watch. Pastikan cargo-watch sudah terinstall: cargo install cargo-watch");
78            
79            if !status.success() {
80                std::process::exit(status.code().unwrap_or(1));
81            }
82        },
83        "make:model" => {
84            if args.len() < 3 {
85                println!("{}", "āŒ Error: Nama model tidak ditentukan.".red().bold());
86                return;
87            }
88            let model_name = &args[2];
89            let with_migration = args.contains(&"-m".to_string());
90            
91            scaffolding::make_model(model_name);
92            if with_migration {
93                scaffolding::make_rust_migration(model_name);
94            }
95        }
96        "make:migration" => {
97            if args.len() < 3 {
98                println!("{}", "āŒ Error: Nama migration tidak ditentukan.".red().bold());
99                return;
100            }
101            scaffolding::make_rust_migration(&args[2]);
102        }
103        "make:controller" => {
104            if args.len() < 3 {
105                println!("{}", "āŒ Error: Nama controller tidak ditentukan.".red().bold());
106                return;
107            }
108            scaffolding::make_controller(&args[2]);
109        }
110        "make:middleware" => {
111            if args.len() < 3 {
112                println!("{}", "āŒ Error: Nama middleware tidak ditentukan.".red().bold());
113                return;
114            }
115            scaffolding::make_middleware(&args[2]);
116        }
117        "migrate" | "migrate:refresh" | "migrate:back" | "migrate:rollback" => {
118            if command == "migrate:refresh" {
119                println!("{}", "šŸ”„ Menyegarkan database (Refresh Migration)...".yellow());
120            } else if command == "migrate:back" || command == "migrate:rollback" {
121                println!("{}", "āŖ Membatalkan migrasi terakhir (Rollback 1 step)...".yellow());
122            } else {
123                println!("{}", "šŸš€ Menjalankan migrasi database...".cyan());
124            }
125            
126            match migrate_fn(command.clone()).await {
127                Ok(_) => {
128                    println!("\n{} {}", "āœ…".green(), format!("Operasi '{}' berhasil diselesaikan.", command).green().bold());
129                }
130                Err(e) => {
131                    eprintln!("\n{} {}", "āŒ Error:".red().bold(), "Gagal menjalankan operasi database.".bold());
132                    eprintln!("{} {}", "šŸ“ Detail:".yellow(), e);
133                    eprintln!("\nšŸ’” {}", "Tips:".cyan().bold());
134                    eprintln!("   Jika muncul error 'Migration file ... is missing', itu berarti database mencatat");
135                    eprintln!("   migrasi yang sudah dijalankan, tapi file migrasinya sudah dihapus atau diubah.");
136                    eprintln!("\nšŸ› ļø  {}", "Solusi:".cyan().bold());
137                    eprintln!("   Hapus file database: 'rm database/rustbasic.sqlite' lalu jalankan migrasi lagi.");
138                    std::process::exit(1);
139                }
140            }
141        }
142        "route:list" => {
143            monitoring::list_routes();
144        }
145        "build" => {
146            builder::build_project();
147            println!("\n{} {}", "āœ…".green(), "Build project berhasil diselesaikan.".green().bold());
148        }
149        "cache:clear" => {
150            database::clear_cache().await;
151        }
152        "check:update" => {
153            monitoring::check_updates();
154            println!("\n{} {}", "āœ…".green(), "Pemeriksaan update selesai.".green().bold());
155        }
156        "check:security" => {
157            monitoring::check_security();
158            println!("\n{} {}", "āœ…".green(), "Audit keamanan selesai.".green().bold());
159        }
160        "key:generate" => {
161            database::generate_app_key();
162        }
163        "make:auth" | "auth" => {
164            if args.len() >= 3 && args[2] == "back" {
165                auth::remove_auth().await;
166                println!("\n{} {}", "āœ…".green(), "Scaffolding autentikasi berhasil dihapus.".green().bold());
167            } else {
168                auth::make_auth().await;
169                println!("\n{} {}", "āœ…".green(), "Scaffolding autentikasi berhasil dibuat.".green().bold());
170            }
171        }
172        "db:seed" => {
173            println!("{}", "🌱 Menjalankan seeder database...".cyan());
174            seed_fn().await;
175            println!("\n{} {}", "āœ…".green(), "Database seeding berhasil diselesaikan.".green().bold());
176        }
177        "make:seeder" => {
178            if args.len() < 3 {
179                println!("{}", "āŒ Error: Nama seeder tidak ditentukan.".red().bold());
180                return;
181            }
182            scaffolding::make_seeder(&args[2]);
183        }
184        "new" => {
185            if args.len() < 3 {
186                println!("{}", "āŒ Error: Nama project tidak ditentukan.".red().bold());
187                println!("Contoh: rustbasic new myapp");
188                return;
189            }
190            let project_name = &args[2];
191
192            // Cek apakah folder sudah ada
193            if std::path::Path::new(project_name).exists() {
194                println!("{} '{}' {}", "āŒ Error: Folder".red().bold(), project_name.yellow(), "sudah ada! Silakan gunakan nama lain.".red().bold());
195                return;
196            }
197
198            println!("\n✨ {} {}", "Membuat project baru:".bold(), project_name.cyan().bold());
199            
200            let status = std::process::Command::new("git")
201                .args(["clone", "https://github.com/herisvan321/rustbasic", project_name])
202                .status();
203
204            match status {
205                Ok(s) if s.success() => {
206                    // Hapus folder .git agar menjadi project baru
207                    let _ = std::process::Command::new("rm")
208                        .args(["-rf", &format!("{}/.git", project_name)])
209                        .status();
210
211                    // Copy .env.example menjadi .env
212                    let env_example = format!("{}/.env.example", project_name);
213                    let env_file = format!("{}/.env", project_name);
214                    if std::path::Path::new(&env_example).exists() {
215                        match std::fs::copy(&env_example, &env_file) {
216                            Ok(_) => println!("   {} .env.example → .env", "šŸ“‹".blue()),
217                            Err(e) => println!("   {} Gagal menyalin .env: {}", "āš ļø".yellow(), e),
218                        }
219                    }
220
221                    // Generate APP_KEY dan jalankan server
222                    if std::path::Path::new(&env_file).exists() {
223                        if std::env::set_current_dir(project_name).is_ok() {
224                            database::generate_app_key();
225
226                            // 3. Download Dependencies (cargo fetch)
227                            println!("šŸ“¦ {} {}", "Mengunduh dependencies...".bold(), "(Ini hanya dilakukan sekali saat inisialisasi)".dimmed());
228                            let _ = std::process::Command::new("cargo")
229                                .args(["fetch"])
230                                .status();
231                            
232                            println!("\nāœ… {} {}", "Project berhasil dibuat!".green().bold(), "Menyiapkan server...".dimmed());
233                            
234                            // Ambil APP_URL dari .env untuk dibuka di browser
235                            let app_url = std::fs::read_to_string(".env")
236                                .unwrap_or_default()
237                                .lines()
238                                .find(|line| line.starts_with("APP_URL="))
239                                .map(|line| line.replace("APP_URL=", ""))
240                                .unwrap_or_else(|| "http://localhost:4000".to_string());
241
242                            // Open browser after compilation is ready
243                            utils::wait_and_open(app_url);
244
245                            // Jalankan serve (sama seperti perintah 'serve')
246                            println!("\n   {} {}", "šŸš€".bold(), "Menjalankan server RustBasic dengan Auto-Reload...".magenta().bold());
247                            let status = std::process::Command::new("cargo")
248                                .args(["watch", "-c", "-q", "--no-ignore", "-i", "target", "-w", "src", "-w", ".env", "-w", "src/resources", "-x", "run"])
249                                .status()
250                                .expect("āŒ Gagal menjalankan cargo watch. Pastikan cargo-watch sudah terinstall: cargo install cargo-watch");
251                            
252                            if !status.success() {
253                                std::process::exit(status.code().unwrap_or(1));
254                            }
255                        }
256                    }
257                }
258                _ => {
259                    println!("{}", "āŒ Gagal mengkloning starter template. Pastikan Anda memiliki koneksi internet dan git terinstall.".red());
260                }
261            }
262        }
263        "auth:back" => {
264            auth::remove_auth().await;
265        }
266       
267        _ => {
268            println!("{} {}", "āŒ Error: Perintah tidak dikenal:".red().bold(), command.yellow());
269            print_help();
270        }
271    }
272}
273
274fn print_help() {
275    println!("\n{}", "šŸ› ļø  RustBasic CLI".magenta().bold());
276    println!("{}", "=================".magenta());
277    println!("{}", "Penggunaan:".bold());
278    println!("  {} {} <Nama>         {}", "rustbasic".blue(), "new".green(), "Membuat project RustBasic baru dari template".dimmed());
279    println!("  {} {} <Nama> [-m]   {}", "rustbasic".blue(), "make:model".green(), "Membuat model Sea-ORM (dan migration Rust)".dimmed());
280    println!("  {} {} <Nama>    {}", "rustbasic".blue(), "make:migration".green(), "Membuat file migration Rust".dimmed());
281    println!("  {} {} <Nama>  {}", "rustbasic".blue(), "make:controller".green(), "Membuat controller Axum".dimmed());
282    println!("  {} {} <Nama>  {}", "rustbasic".blue(), "make:middleware".green(), "Membuat middleware Axum".dimmed());
283    println!("  {} {}                  {}", "rustbasic".blue(), "migrate".green(), "Menjalankan migrasi database (Sea-ORM)".dimmed());
284    println!("  {} {}          {}", "rustbasic".blue(), "migrate:refresh".green(), "Rollback semua dan jalankan kembali migrasi".dimmed());
285    println!("  {} {}             {}", "rustbasic".blue(), "migrate:back".green(), "Membatalkan migrasi terakhir (Rollback)".dimmed());
286    println!("  {} {}               {}", "rustbasic".blue(), "route:list".green(), "Menampilkan daftar route".dimmed());
287    println!("  {} {}                    {}", "rustbasic".blue(), "build".green(), "Membangun project dengan pilihan".dimmed());
288    println!("  {} {}             {}", "rustbasic".blue(), "check:update".green(), "Cek versi terbaru paket (dependencies)".dimmed());
289    println!("  {} {}           {}", "rustbasic".blue(), "check:security".green(), "Audit keamanan aplikasi".dimmed());
290    println!("  {} {}               {}", "rustbasic".blue(), "cache:clear".green(), "Membersihkan logs dan database sessions".dimmed());
291    println!("  {} {}             {}", "rustbasic".blue(), "key:generate".green(), "Membuat APP_KEY baru di file .env".dimmed());
292    println!("  {} {}                   {}", "rustbasic".blue(), "make:auth".green(), "Scaffold autentikasi (Login/Register)".dimmed());
293    println!("  {} {}                   {}", "rustbasic".blue(), "auth:back".red(), "Menghapus semua scaffolding autentikasi".dimmed());
294    println!("  {} {}                  {}", "rustbasic".blue(), "db:seed".green(), "Menjalankan seeder database".dimmed());
295    println!("  {} {} <Nama>    {}", "rustbasic".blue(), "make:seeder".green(), "Membuat file seeder baru".dimmed());
296    println!("  {} {}                   {}", "rustbasic".blue(), "serve".green(), "Menjalankan server dengan Auto-Reload".dimmed());
297    println!("  {} {}                 {}", "rustbasic".blue(), "version".green(), "Menampilkan versi CLI saat ini".dimmed());
298    println!("  {} {}                      {}", "rustbasic".blue(), "-v".green(), "Shortcut untuk menampilkan versi".dimmed());
299    println!("  {} {}                  {}", "rustbasic".blue(), "cargo serve".green(), "(Shortcut) Lebih cepat untuk menjalankan server".dimmed());
300
301    println!();
302}