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 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 }
54 }
55 }
56
57 if command != "new" {
59 let _ = dotenv(); }
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 let host = env::var("APP_HOST").unwrap_or_else(|_| "localhost".to_string());
72 let port = env::var("APP_PORT").unwrap_or_else(|_| "4000".to_string());
73
74 let display_host = if host == "0.0.0.0" { "localhost".to_string() } else { host };
76 let app_url = format!("http://{}:{}", display_host, port);
77
78 utils::wait_and_open(app_url);
79
80 let status = std::process::Command::new("cargo")
81 .args(["watch", "-c", "-q", "--no-ignore", "-i", "target", "-w", "src", "-w", ".env", "-w", "src/resources", "-x", "run"])
82 .status()
83 .expect("ā Gagal menjalankan cargo watch. Pastikan cargo-watch sudah terinstall: cargo install cargo-watch");
84
85 if !status.success() {
86 std::process::exit(status.code().unwrap_or(1));
87 }
88 },
89 "make:model" => {
90 if args.len() < 3 {
91 println!("{}", "ā Error: Nama model tidak ditentukan.".red().bold());
92 return;
93 }
94 let model_name = &args[2];
95 let with_migration = args.contains(&"-m".to_string());
96
97 scaffolding::make_model(model_name);
98 if with_migration {
99 scaffolding::make_rust_migration(model_name);
100 }
101 }
102 "make:migration" => {
103 if args.len() < 3 {
104 println!("{}", "ā Error: Nama migration tidak ditentukan.".red().bold());
105 return;
106 }
107 scaffolding::make_rust_migration(&args[2]);
108 }
109 "make:controller" => {
110 if args.len() < 3 {
111 println!("{}", "ā Error: Nama controller tidak ditentukan.".red().bold());
112 return;
113 }
114 scaffolding::make_controller(&args[2]);
115 }
116 "make:middleware" => {
117 if args.len() < 3 {
118 println!("{}", "ā Error: Nama middleware tidak ditentukan.".red().bold());
119 return;
120 }
121 scaffolding::make_middleware(&args[2]);
122 }
123 "migrate" | "migrate:refresh" | "migrate:back" | "migrate:rollback" => {
124 if command == "migrate:refresh" {
125 println!("{}", "š Menyegarkan database (Refresh Migration)...".yellow());
126 } else if command == "migrate:back" || command == "migrate:rollback" {
127 println!("{}", "āŖ Membatalkan migrasi terakhir (Rollback 1 step)...".yellow());
128 } else {
129 println!("{}", "š Menjalankan migrasi database...".cyan());
130 }
131
132 match migrate_fn(command.clone()).await {
133 Ok(_) => {
134 println!("\n{} {}", "ā
".green(), format!("Operasi '{}' berhasil diselesaikan.", command).green().bold());
135 }
136 Err(e) => {
137 eprintln!("\n{} {}", "ā Error:".red().bold(), "Gagal menjalankan operasi database.".bold());
138 eprintln!("{} {}", "š Detail:".yellow(), e);
139 eprintln!("\nš” {}", "Tips:".cyan().bold());
140 eprintln!(" Jika muncul error 'Migration file ... is missing', itu berarti database mencatat");
141 eprintln!(" migrasi yang sudah dijalankan, tapi file migrasinya sudah dihapus atau diubah.");
142 eprintln!("\nš ļø {}", "Solusi:".cyan().bold());
143 eprintln!(" Hapus file database: 'rm database/rustbasic.sqlite' lalu jalankan migrasi lagi.");
144 std::process::exit(1);
145 }
146 }
147 }
148 "route:list" => {
149 monitoring::list_routes();
150 }
151 "build" => {
152 builder::build_project();
153 println!("\n{} {}", "ā
".green(), "Build project berhasil diselesaikan.".green().bold());
154 }
155 "cache:clear" => {
156 database::clear_cache().await;
157 }
158 "check:update" => {
159 monitoring::check_updates();
160 println!("\n{} {}", "ā
".green(), "Pemeriksaan update selesai.".green().bold());
161 }
162 "check:security" => {
163 monitoring::check_security();
164 println!("\n{} {}", "ā
".green(), "Audit keamanan selesai.".green().bold());
165 }
166 "key:generate" => {
167 database::generate_app_key();
168 }
169 "make:auth" | "auth" => {
170 if args.len() >= 3 && args[2] == "back" {
171 auth::remove_auth().await;
172 println!("\n{} {}", "ā
".green(), "Scaffolding autentikasi berhasil dihapus.".green().bold());
173 } else {
174 auth::make_auth().await;
175 println!("\n{} {}", "ā
".green(), "Scaffolding autentikasi berhasil dibuat.".green().bold());
176 }
177 }
178 "db:seed" => {
179 println!("{}", "š± Menjalankan seeder database...".cyan());
180 seed_fn().await;
181 println!("\n{} {}", "ā
".green(), "Database seeding berhasil diselesaikan.".green().bold());
182 }
183 "make:seeder" => {
184 if args.len() < 3 {
185 println!("{}", "ā Error: Nama seeder tidak ditentukan.".red().bold());
186 return;
187 }
188 scaffolding::make_seeder(&args[2]);
189 }
190 "new" => {
191 if args.len() < 3 {
192 println!("{}", "ā Error: Nama project tidak ditentukan.".red().bold());
193 println!("Contoh: rustbasic new myapp");
194 return;
195 }
196 let project_name = &args[2];
197
198 if std::path::Path::new(project_name).exists() {
200 println!("{} '{}' {}", "ā Error: Folder".red().bold(), project_name.yellow(), "sudah ada! Silakan gunakan nama lain.".red().bold());
201 return;
202 }
203
204 println!("\n⨠{} {}", "Membuat project baru:".bold(), project_name.cyan().bold());
205
206 let status = std::process::Command::new("git")
207 .args(["clone", "https://github.com/herisvan321/rustbasic", project_name])
208 .status();
209
210 match status {
211 Ok(s) if s.success() => {
212 let _ = std::process::Command::new("rm")
214 .args(["-rf", &format!("{}/.git", project_name)])
215 .status();
216
217 let env_example = format!("{}/.env.example", project_name);
219 let env_file = format!("{}/.env", project_name);
220 if std::path::Path::new(&env_example).exists() {
221 match std::fs::copy(&env_example, &env_file) {
222 Ok(_) => println!(" {} .env.example ā .env", "š".blue()),
223 Err(e) => println!(" {} Gagal menyalin .env: {}", "ā ļø".yellow(), e),
224 }
225 }
226
227 if std::path::Path::new(&env_file).exists() {
229 if std::env::set_current_dir(project_name).is_ok() {
230 database::generate_app_key();
231
232 println!("š¦ {} {}", "Mengunduh dependencies...".bold(), "(Ini hanya dilakukan sekali saat inisialisasi)".dimmed());
234 let _ = std::process::Command::new("cargo")
235 .args(["fetch"])
236 .status();
237
238 println!("\nā
{} {}", "Project berhasil dibuat!".green().bold(), "Menyiapkan server...".dimmed());
239
240 let env_content = std::fs::read_to_string(".env").unwrap_or_default();
242 let host = env_content.lines()
243 .find(|line| line.starts_with("APP_HOST="))
244 .map(|line| line.replace("APP_HOST=", ""))
245 .unwrap_or_else(|| "localhost".to_string());
246 let port = env_content.lines()
247 .find(|line| line.starts_with("APP_PORT="))
248 .map(|line| line.replace("APP_PORT=", ""))
249 .unwrap_or_else(|| "4000".to_string());
250
251 let display_host = if host == "0.0.0.0" { "localhost".to_string() } else { host };
252 let app_url = format!("http://{}:{}", display_host, port);
253
254 utils::wait_and_open(app_url);
256
257 println!("\n {} {}", "š".bold(), "Menjalankan server RustBasic dengan Auto-Reload...".magenta().bold());
259 let status = std::process::Command::new("cargo")
260 .args(["watch", "-c", "-q", "--no-ignore", "-i", "target", "-w", "src", "-w", ".env", "-w", "src/resources", "-x", "run"])
261 .status()
262 .expect("ā Gagal menjalankan cargo watch. Pastikan cargo-watch sudah terinstall: cargo install cargo-watch");
263
264 if !status.success() {
265 std::process::exit(status.code().unwrap_or(1));
266 }
267 }
268 }
269 }
270 _ => {
271 println!("{}", "ā Gagal mengkloning starter template. Pastikan Anda memiliki koneksi internet dan git terinstall.".red());
272 }
273 }
274 }
275 "auth:back" => {
276 auth::remove_auth().await;
277 }
278
279 _ => {
280 println!("{} {}", "ā Error: Perintah tidak dikenal:".red().bold(), command.yellow());
281 print_help();
282 }
283 }
284}
285
286fn print_help() {
287 println!("\n{}", "š ļø RustBasic CLI".magenta().bold());
288 println!("{}", "=================".magenta());
289 println!("{}", "Penggunaan:".bold());
290 println!(" {} {} <Nama> {}", "rustbasic".blue(), "new".green(), "Membuat project RustBasic baru dari template".dimmed());
291 println!(" {} {} <Nama> [-m] {}", "rustbasic".blue(), "make:model".green(), "Membuat model Sea-ORM (dan migration Rust)".dimmed());
292 println!(" {} {} <Nama> {}", "rustbasic".blue(), "make:migration".green(), "Membuat file migration Rust".dimmed());
293 println!(" {} {} <Nama> {}", "rustbasic".blue(), "make:controller".green(), "Membuat controller Axum".dimmed());
294 println!(" {} {} <Nama> {}", "rustbasic".blue(), "make:middleware".green(), "Membuat middleware Axum".dimmed());
295 println!(" {} {} {}", "rustbasic".blue(), "migrate".green(), "Menjalankan migrasi database (Sea-ORM)".dimmed());
296 println!(" {} {} {}", "rustbasic".blue(), "migrate:refresh".green(), "Rollback semua dan jalankan kembali migrasi".dimmed());
297 println!(" {} {} {}", "rustbasic".blue(), "migrate:back".green(), "Membatalkan migrasi terakhir (Rollback)".dimmed());
298 println!(" {} {} {}", "rustbasic".blue(), "route:list".green(), "Menampilkan daftar route".dimmed());
299 println!(" {} {} {}", "rustbasic".blue(), "build".green(), "Membangun project dengan pilihan".dimmed());
300 println!(" {} {} {}", "rustbasic".blue(), "check:update".green(), "Cek versi terbaru paket (dependencies)".dimmed());
301 println!(" {} {} {}", "rustbasic".blue(), "check:security".green(), "Audit keamanan aplikasi".dimmed());
302 println!(" {} {} {}", "rustbasic".blue(), "cache:clear".green(), "Membersihkan logs dan database sessions".dimmed());
303 println!(" {} {} {}", "rustbasic".blue(), "key:generate".green(), "Membuat APP_KEY baru di file .env".dimmed());
304 println!(" {} {} {}", "rustbasic".blue(), "make:auth".green(), "Scaffold autentikasi (Login/Register)".dimmed());
305 println!(" {} {} {}", "rustbasic".blue(), "auth:back".red(), "Menghapus semua scaffolding autentikasi".dimmed());
306 println!(" {} {} {}", "rustbasic".blue(), "db:seed".green(), "Menjalankan seeder database".dimmed());
307 println!(" {} {} <Nama> {}", "rustbasic".blue(), "make:seeder".green(), "Membuat file seeder baru".dimmed());
308 println!(" {} {} {}", "rustbasic".blue(), "serve".green(), "Menjalankan server dengan Auto-Reload".dimmed());
309 println!(" {} {} {}", "rustbasic".blue(), "version".green(), "Menampilkan versi CLI saat ini".dimmed());
310 println!(" {} {} {}", "rustbasic".blue(), "-v".green(), "Shortcut untuk menampilkan versi".dimmed());
311 println!(" {} {} {}", "rustbasic".blue(), "cargo serve".green(), "(Shortcut) Lebih cepat untuk menjalankan server".dimmed());
312
313 println!();
314}