Skip to main content

rustbasic_core/
cli.rs

1use crate::app::Config;
2use crate::schema::MigratorTrait;
3use crate::seeder::SeederTrait;
4use crate::colored::Colorize;
5
6/// Entry point CLI utama — dipanggil oleh project's cli.rs
7///
8/// M = MigratorTrait  (dari project/database/migrations)
9/// S = SeederTrait    (dari project/database/seeders)
10pub async fn handle<
11    M: MigratorTrait + Send + Sync + 'static,
12    S: SeederTrait + Send + Sync + 'static,
13>(
14    args: &[String],
15    cfg: &Config,
16    seeder: Option<S>,
17) -> bool {
18    if args.len() < 2 {
19        return false;
20    }
21
22    let command = args[1].as_str();
23
24    let is_migration_cmd = command.starts_with("migrate") || command == "db:seed";
25    let is_storage_cmd   = command == "storage:link";
26    let is_server_cmd    = command == "server" || command == "serve";
27    let is_build_cmd     = command == "build";
28    let is_make_cmd      = command.starts_with("make:");
29    let is_route_cmd     = command == "route:list";
30    let is_key_cmd       = command == "key:generate";
31    let is_deploy_cmd    = command == "deploy";
32    let is_publish_cmd   = command == "publish";
33
34    // server --android / --desktop → jalankan native runner
35    if is_server_cmd {
36        let run_android = args.iter().any(|arg| arg == "--android");
37        let run_desktop = args.iter().any(|arg| arg == "--desktop");
38        if run_android || run_desktop {
39            run_native(run_android, run_desktop);
40            return true;
41        }
42        return false; // fall through ke standard web server
43    }
44
45    if !is_migration_cmd && !is_storage_cmd && !is_build_cmd
46        && !is_make_cmd && !is_route_cmd && !is_key_cmd && !is_deploy_cmd
47        && !is_publish_cmd {
48        return false;
49    }
50
51    println!("šŸ› ļø  RustBasic CLI - Command: {}", command);
52
53    if is_build_cmd {
54        handle_build(args).await;
55        return true;
56    }
57
58    if is_deploy_cmd {
59        handle_deploy().await;
60        return true;
61    }
62
63    if is_storage_cmd {
64        handle_storage_link(cfg);
65        return true;
66    }
67
68    if is_make_cmd {
69        handle_make(args);
70        return true;
71    }
72
73    if is_route_cmd {
74        handle_route_list();
75        return true;
76    }
77
78    if is_key_cmd {
79        handle_key_generate();
80        return true;
81    }
82
83    if is_publish_cmd {
84        let target = args.get(2).map(|s| s.as_str()).unwrap_or("");
85        handle_publish(target);
86        return true;
87    }
88
89    // Perintah database — perlu koneksi pool
90    let pool = crate::database::connect(cfg).await;
91
92    match command {
93        "migrate" => {
94            println!("šŸš€ Menjalankan migrasi database...");
95            if let Err(e) = M::up(&pool, None).await {
96                println!("āŒ Gagal menjalankan migrasi: {}", e);
97            } else {
98                println!("āœ… Migrasi selesai!");
99            }
100        }
101        "migrate:refresh" => {
102            println!("šŸ”„ Mereset dan menjalankan ulang migrasi...");
103            if let Err(e) = M::fresh(&pool).await {
104                println!("āŒ Gagal refresh migrasi: {}", e);
105            } else {
106                println!("āœ… Database berhasil di-refresh!");
107            }
108        }
109        "migrate:back" | "migrate:rollback" => {
110            println!("ā¬…ļø  Rollback migrasi terakhir...");
111            if let Err(e) = M::down(&pool, None).await {
112                println!("āŒ Gagal rollback: {}", e);
113            } else {
114                println!("āœ… Rollback berhasil!");
115            }
116        }
117        "db:seed" => {
118            println!("🌱 Menjalankan database seeder...");
119            match seeder {
120                Some(s) => {
121                    if let Err(e) = s.run(&pool).await {
122                        println!("āŒ Seeder gagal: {}", e);
123                    } else {
124                        println!("āœ… Seeder selesai!");
125                    }
126                }
127                None => {
128                    println!("āš ļø  Tidak ada seeder yang terdaftar.");
129                }
130            }
131        }
132        _ => return false,
133    }
134
135    true
136}
137
138// ============================================================
139// KEY:GENERATE
140// ============================================================
141fn handle_key_generate() {
142    let key = crate::rand::random_alphanumeric(32);
143    let encoded = crate::base64::encode(key.as_bytes());
144
145    // Baca .env, update APP_KEY, tulis balik
146    let env_path = std::path::Path::new(".env");
147    if env_path.exists() {
148        let content = std::fs::read_to_string(env_path).unwrap_or_default();
149        let new_content = if content.contains("APP_KEY=") {
150            let mut lines: Vec<String> = content
151                .lines()
152                .map(|line| {
153                    if line.starts_with("APP_KEY=") {
154                        format!("APP_KEY={}", encoded)
155                    } else {
156                        line.to_string()
157                    }
158                })
159                .collect();
160            // pastikan ada newline di akhir
161            if !lines.last().map(|l| l.is_empty()).unwrap_or(true) {
162                lines.push(String::new());
163            }
164            lines.join("\n")
165        } else {
166            format!("{}\nAPP_KEY={}\n", content.trim_end(), encoded)
167        };
168        if let Err(e) = std::fs::write(env_path, new_content) {
169            println!("āŒ Gagal menulis ke .env: {}", e);
170            return;
171        }
172        println!("āœ… APP_KEY berhasil dibuat dan disimpan ke .env");
173        println!("   Key: {}", encoded);
174    } else {
175        println!("āš ļø  File .env tidak ditemukan. Key yang dibuat:");
176        println!("   APP_KEY={}", encoded);
177    }
178}
179
180// ============================================================
181// ROUTE:LIST
182// ============================================================
183fn handle_route_list() {
184    // Baca route definitions dari src/routes/ secara statis (parse file)
185    let route_files = ["src/routes/web.rs", "src/routes/api.rs"];
186    let mut routes: Vec<(String, String, String)> = Vec::new(); // (method, path, handler)
187
188    for file_path in &route_files {
189        if let Ok(content) = std::fs::read_to_string(file_path) {
190            for line in content.lines() {
191                let line = line.trim();
192                // Match pola: .route("/path", get(handler)).name("x")
193                // atau: .route("/path", post(handler))
194                if line.starts_with(".route(") || line.contains(".route(\"") {
195                    parse_route_line(line, &mut routes);
196                }
197            }
198        }
199    }
200
201    if routes.is_empty() {
202        println!("ā„¹ļø  Tidak ada rute ditemukan atau format rute tidak dikenal.");
203        println!("   Cek file src/routes/web.rs dan src/routes/api.rs");
204        return;
205    }
206
207    // Header tabel
208    println!("\n{}", "=".repeat(72));
209    println!("  {:<8} {:<35} {}", "METHOD", "PATH", "HANDLER");
210    println!("{}", "=".repeat(72));
211    for (method, path, handler) in &routes {
212        println!("  {:<8} {:<35} {}", method, path, handler);
213    }
214    println!("{}\n", "=".repeat(72));
215    println!("  Total: {} rute terdaftar", routes.len());
216}
217
218fn parse_route_line(line: &str, routes: &mut Vec<(String, String, String)>) {
219    // Cari path string (content antara tanda kutip pertama)
220    let methods = ["get", "post", "put", "patch", "delete"];
221    
222    // Ekstrak path dari .route("path", ...)
223    let path = if let Some(start) = line.find(".route(\"") {
224        let after = &line[start + 8..];
225        if let Some(end) = after.find('"') {
226            after[..end].to_string()
227        } else { return; }
228    } else { return; };
229
230    // Ekstrak method dan handler
231    for method in &methods {
232        let pattern = format!("{}(", method);
233        if let Some(pos) = line.find(&pattern) {
234            let after = &line[pos + method.len() + 1..];
235            let handler = if let Some(end) = after.find(')') {
236                after[..end].to_string()
237            } else {
238                after.to_string()
239            };
240            routes.push((method.to_uppercase(), path.clone(), handler));
241            break;
242        }
243    }
244}
245
246// ============================================================
247// MAKE:* GENERATOR
248// ============================================================
249fn handle_make(args: &[String]) {
250    let subcommand = args[1].as_str(); // e.g. "make:controller"
251    let name = args.get(2).map(|s| s.as_str()).unwrap_or("");
252
253    if name.is_empty() {
254        println!("āŒ Nama diperlukan. Contoh: rustbasic {} MyName", subcommand);
255        return;
256    }
257
258    match subcommand {
259        "make:controller" => make_controller(name),
260        "make:model" => {
261            let with_migration = args.iter().any(|a| a == "-m" || a == "--migration");
262            make_model(name, with_migration);
263        }
264        "make:middleware" => make_middleware(name),
265        "make:observer" => {
266            let model = args.iter().find(|a| a.starts_with("--model="))
267                .and_then(|a| a.strip_prefix("--model="))
268                .unwrap_or("Model");
269            make_observer(name, model);
270        }
271        "make:service" => make_service(name),
272        "make:seeder"  => make_seeder(name),
273        "make:migration" => make_migration(name),
274        _ => {
275            println!("āŒ Subcommand tidak dikenal: {}", subcommand);
276        }
277    }
278}
279
280/// Konversi PascalCase → snake_case
281fn to_snake_case(s: &str) -> String {
282    let mut out = String::new();
283    for (i, c) in s.chars().enumerate() {
284        if c.is_uppercase() && i > 0 {
285            out.push('_');
286        }
287        out.push(c.to_ascii_lowercase());
288    }
289    out
290}
291
292/// Tulis file — buat folder parent jika belum ada
293fn write_file(path: &str, content: &str) {
294    let p = std::path::Path::new(path);
295    if let Some(parent) = p.parent() {
296        if let Err(e) = std::fs::create_dir_all(parent) {
297            println!("āŒ Gagal membuat direktori {}: {}", parent.display(), e);
298            return;
299        }
300    }
301    if p.exists() {
302        println!("āš ļø  File sudah ada, dilewati: {}", path);
303        return;
304    }
305    if let Err(e) = std::fs::write(p, content) {
306        println!("āŒ Gagal membuat file {}: {}", path, e);
307    } else {
308        println!("āœ… Dibuat: {}", path);
309    }
310}
311
312/// Tambahkan baris `pub mod <name>;` ke mod.rs jika belum ada
313fn register_mod(mod_file: &str, module_name: &str) {
314    let line = format!("pub mod {};\n", module_name);
315    let content = std::fs::read_to_string(mod_file).unwrap_or_default();
316    if content.contains(&line) { return; }
317
318    let label = "// šŸ“‘ LABEL: MODULE";
319    let new_content = if let Some(pos) = content.find(label) {
320        // Sisipkan setelah baris label
321        let nl = content[pos..].find('\n').map(|n| pos + n + 1).unwrap_or(content.len());
322        format!("{}{}{}", &content[..nl], &line, &content[nl..])
323    } else {
324        format!("{}{}", content, &line)
325    };
326
327    if let Err(e) = std::fs::write(mod_file, new_content) {
328        println!("āš ļø  Gagal mendaftarkan modul di {}: {}", mod_file, e);
329    } else {
330        println!("   šŸ“ Daftar di: {}", mod_file);
331    }
332}
333
334fn make_controller(name: &str) {
335    let snake = to_snake_case(name);
336    let file_name = if snake.ends_with("_controller") {
337        snake.clone()
338    } else {
339        format!("{}_controller", snake)
340    };
341    let struct_name = name.to_string();
342    let path = format!("src/app/http/controllers/{}.rs", file_name);
343
344    let content = format!(r#"use rustbasic_core::requests::Request;
345use rustbasic_core::IntoResponse;
346
347pub async fn index(_req: Request) -> impl IntoResponse {{
348    "Hello from {struct_name}Controller!"
349}}
350
351pub async fn show(_req: Request) -> impl IntoResponse {{
352    "Show action"
353}}
354
355pub async fn store(_req: Request) -> impl IntoResponse {{
356    "Store action"
357}}
358
359pub async fn update(_req: Request) -> impl IntoResponse {{
360    "Update action"
361}}
362
363pub async fn destroy(_req: Request) -> impl IntoResponse {{
364    "Destroy action"
365}}
366"#);
367
368    write_file(&path, &content);
369    register_mod("src/app/http/controllers/mod.rs", &file_name);
370}
371
372fn make_model(name: &str, with_migration: bool) {
373    let snake = to_snake_case(name);
374    let path = format!("src/app/models/{}.rs", snake);
375
376    let content = format!(r#"use rustbasic_core::model;
377
378model! {{
379    table: "{snake}s",
380    fillable: [],
381    Model {{
382        pub id: i32,
383        pub created_at: Option<String>,
384        pub updated_at: Option<String>,
385    }}
386}}
387
388impl Model {{
389    pub fn to_resource(&self) -> rustbasic_core::serde_json::Value {{
390        rustbasic_core::serde_json::json!({{
391            "id": self.id,
392        }})
393    }}
394}}
395"#);
396
397    write_file(&path, &content);
398    register_mod("src/app/models/mod.rs", &snake);
399
400    if with_migration {
401        make_migration(&format!("create_{}_table", snake));
402    }
403}
404
405fn make_middleware(name: &str) {
406    let snake = to_snake_case(name);
407    let path = format!("src/app/http/middleware/{}.rs", snake);
408
409    let content = format!(r#"use rustbasic_core::requests::Request;
410use rustbasic_core::router::{{Response, Next}};
411
412pub async fn {snake}_middleware(req: Request, next: Next) -> Response {{
413    // Logika middleware sebelum handler
414    let res = next.run(req).await;
415    // Logika middleware setelah handler
416    res
417}}
418"#);
419
420    write_file(&path, &content);
421    register_mod("src/app/http/middleware/mod.rs", &snake);
422}
423
424fn make_observer(name: &str, model: &str) {
425    let snake = to_snake_case(name);
426    let obs_name = if snake.ends_with("_observer") {
427        snake.clone()
428    } else {
429        format!("{}_observer", snake)
430    };
431    let path = format!("src/app/observers/{}.rs", obs_name);
432
433    let content = format!(r#"use crate::app::models::{snake}::Model as {model};
434
435pub struct {name}Observer;
436
437impl {name}Observer {{
438    pub async fn created(&self, model: &{model}) {{
439        // Dipanggil setelah model dibuat
440        let _ = model;
441    }}
442
443    pub async fn updated(&self, model: &{model}) {{
444        // Dipanggil setelah model diupdate
445        let _ = model;
446    }}
447
448    pub async fn deleted(&self, model: &{model}) {{
449        // Dipanggil setelah model dihapus
450        let _ = model;
451    }}
452}}
453"#);
454
455    write_file(&path, &content);
456}
457
458fn make_service(name: &str) {
459    let snake = to_snake_case(name);
460    let svc_name = if snake.ends_with("_service") {
461        snake.clone()
462    } else {
463        format!("{}_service", snake)
464    };
465    let path = format!("src/app/services/{}.rs", svc_name);
466
467    let content = format!(r#"use rustbasic_core::sql::AnyPool;
468
469pub struct {name}Service<'a> {{
470    pub db: &'a AnyPool,
471}}
472
473impl<'a> {name}Service<'a> {{
474    pub fn new(db: &'a AnyPool) -> Self {{
475        Self {{ db }}
476    }}
477
478    pub async fn execute(&self) -> Result<(), String> {{
479        // Implementasi logika bisnis di sini
480        Ok(())
481    }}
482}}
483"#);
484
485    write_file(&path, &content);
486}
487
488fn make_seeder(name: &str) {
489    let snake = to_snake_case(name);
490    let seed_name = if snake.ends_with("_seeder") {
491        snake.clone()
492    } else {
493        format!("{}_seeder", snake)
494    };
495    let path = format!("database/seeders/{}.rs", seed_name);
496
497    let content = format!(r#"use rustbasic_core::sql::AnyPool;
498
499pub async fn run(db: &AnyPool) {{
500    println!("🌱 Menjalankan {}...");
501    // Contoh:
502    // rustbasic_core::database::execute(db, "INSERT INTO ...", ()).await;
503}}
504"#, seed_name);
505
506    write_file(&path, &content);
507}
508
509fn make_migration(name: &str) {
510    use std::time::{SystemTime, UNIX_EPOCH};
511
512    let ts = SystemTime::now()
513        .duration_since(UNIX_EPOCH)
514        .map(|d| d.as_secs())
515        .unwrap_or(0);
516
517    // Format timestamp: m{ts}_<name>
518    let migration_name = format!("m{}_{}", ts, to_snake_case(name));
519    let path = format!("database/migrations/{}.rs", migration_name);
520
521    let content = format!(r#"use rustbasic_core::schema::{{Schema, MigrationTrait, DbErr}};
522use rustbasic_core::sql::AnyPool;
523use rustbasic_core::async_trait;
524
525pub struct {migration_name};
526
527#[async_trait]
528impl MigrationTrait for {migration_name} {{
529    async fn up(&self, db: &AnyPool) -> Result<(), DbErr> {{
530        Schema::create("{}", db, |t| {{
531            t.id();
532            t.timestamps();
533        }}).await
534    }}
535
536    async fn down(&self, db: &AnyPool) -> Result<(), DbErr> {{
537        Schema::drop_if_exists("{}", db).await
538    }}
539}}
540"#, to_snake_case(name), to_snake_case(name));
541
542    write_file(&path, &content);
543}
544
545// ============================================================
546// STORAGE:LINK
547// ============================================================
548fn handle_storage_link(cfg: &Config) {
549    let target = "public/storage";
550    let source = "storage/app/public";
551
552    if let Err(e) = std::fs::create_dir_all(source) {
553        println!("āŒ Gagal membuat direktori storage: {}", e);
554        return;
555    }
556
557    let path = std::path::Path::new(target);
558    if path.exists() || path.is_symlink() {
559        println!("ā„¹ļø  Link 'public/storage' sudah ada.");
560        return;
561    }
562
563    println!("šŸ”— Membuat symbolic link...");
564
565    #[cfg(unix)]
566    {
567        use std::os::unix::fs::symlink;
568        if let Err(e) = symlink("../storage/app/public", target) {
569            println!("āŒ Gagal membuat symlink: {}", e);
570        } else {
571            println!("āœ… Link storage berhasil! [public/storage -> storage/app/public]");
572        }
573    }
574
575    #[cfg(windows)]
576    {
577        use std::os::windows::fs::symlink_dir;
578        if let Err(e) = symlink_dir("../storage/app/public", target) {
579            println!("āŒ Gagal membuat symlink: {}", e);
580        } else {
581            println!("āœ… Link storage berhasil! [public/storage -> storage/app/public]");
582        }
583    }
584
585    let _ = cfg;
586}
587
588// ============================================================
589// RUN NATIVE (server --android/--desktop)
590// ============================================================
591fn setup_java_home() {
592    if std::env::var("JAVA_HOME").is_err() {
593        let mut custom_java_home: Option<String> = None;
594        let os = std::env::consts::OS;
595        if os == "macos" {
596            let paths = vec![
597                "/Applications/Android Studio.app/Contents/jbr/Contents/Home",
598                "/Applications/Android Studio.app/Contents/jre/Contents/Home",
599                "/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home",
600            ];
601            for path in &paths {
602                if std::path::Path::new(path).exists() {
603                    custom_java_home = Some(path.to_string());
604                    break;
605                }
606            }
607        } else if os == "windows" {
608            let win_paths = [
609                "C:\\Program Files\\Android\\Android Studio\\jbr",
610                "C:\\Program Files\\Android\\Android Studio\\jre",
611            ];
612            for path in &win_paths {
613                if std::path::Path::new(path).exists() {
614                    custom_java_home = Some(path.to_string());
615                    break;
616                }
617            }
618        } else {
619            // Linux & other Unix-like OS
620            let unix_paths = [
621                "/opt/android-studio/jbr",
622                "/opt/android-studio/jre",
623                "/snap/android-studio/current/jbr",
624                "/snap/android-studio/current/jre",
625                "/usr/local/android-studio/jbr",
626                "/usr/local/android-studio/jre",
627                "/usr/lib/jvm/default-java",
628            ];
629            for path in &unix_paths {
630                if std::path::Path::new(path).exists() {
631                    custom_java_home = Some(path.to_string());
632                    break;
633                }
634            }
635        }
636        if let Some(jh) = custom_java_home {
637            unsafe {
638                std::env::set_var("JAVA_HOME", &jh);
639            }
640        }
641    }
642}
643
644fn run_native(run_android: bool, run_desktop: bool) {
645    if run_android {
646        println!("šŸš€ Memulai RustBasic Android Wrapper (Native implementation)...");
647
648        // 1. Setup environment
649        let home = std::env::var("HOME").unwrap_or_default();
650        let android_home = std::env::var("ANDROID_HOME")
651            .unwrap_or_else(|_| format!("{}/Library/Android/sdk", home));
652        unsafe { std::env::set_var("ANDROID_HOME", &android_home); }
653
654        setup_java_home();
655
656        let mut devices = get_adb_devices();
657        if devices.is_empty() {
658            println!("šŸ“± Perangkat Android atau emulator tidak terdeteksi aktif.");
659            let emulator_bin = format!("{}/emulator/emulator", android_home);
660            if std::path::Path::new(&emulator_bin).exists() {
661                let avd_output = std::process::Command::new(&emulator_bin).arg("-list-avds").output();
662                if let Ok(avd_out) = avd_output {
663                    let avds_str = String::from_utf8_lossy(&avd_out.stdout);
664                    if let Some(avd_name) = avds_str.lines().next() {
665                        println!("šŸš€ Menyalakan emulator AVD: {}...", avd_name);
666                        let _ = std::process::Command::new(&emulator_bin)
667                            .args(["-avd", avd_name])
668                            .stdout(std::process::Stdio::null())
669                            .stderr(std::process::Stdio::null())
670                            .spawn();
671                        
672                        println!("ā³ Menunggu emulator menyala dan terdeteksi adb...");
673                        let _ = std::process::Command::new("adb").arg("wait-for-device").status();
674                        println!("āœ… Emulator berhasil aktif!");
675                        std::thread::sleep(std::time::Duration::from_secs(3));
676                        devices = get_adb_devices();
677                    }
678                }
679            }
680        }
681
682        let (device_id, device_name) = if devices.len() == 1 {
683            let d = devices[0].clone();
684            println!("šŸ“± Menggunakan perangkat tunggal: {} ({})", d.1, d.0);
685            d
686        } else if devices.len() > 1 {
687            println!("šŸ“± Terdeteksi beberapa perangkat Android. Silakan pilih target:");
688            for (idx, d) in devices.iter().enumerate() {
689                println!("  [{}] {} ({})", idx + 1, d.1, d.0);
690            }
691            let choice = prompt_choice("šŸ‘‰ Pilih nomor perangkat: ", 1, devices.len());
692            devices[choice - 1].clone()
693        } else {
694            println!("āŒ Error: Tidak ada perangkat Android terhubung.");
695            return;
696        };
697
698        // 3. build JNI
699        if !compile_jni_libraries() {
700            return;
701        }
702
703        // 4. local.properties
704        let local_props = std::path::Path::new("native/android/local.properties");
705        if !local_props.exists() {
706            if let Ok(mut file) = std::fs::File::create(local_props) {
707                use std::io::Write;
708                let _ = writeln!(file, "sdk.dir={}", android_home);
709            }
710        }
711
712        // 5. gradlew assembleDebug
713        println!("šŸ”Ø Membangun debug APK menggunakan Gradle...");
714        let gradlew_bin = if cfg!(target_os = "windows") { "gradlew.bat" } else { "./gradlew" };
715        let mut gradle_cmd = std::process::Command::new(gradlew_bin);
716        gradle_cmd.arg("assembleDebug");
717        gradle_cmd.current_dir("native/android");
718
719        if let Ok(jh) = std::env::var("JAVA_HOME") {
720            gradle_cmd.env("JAVA_HOME", jh);
721        }
722
723        let gradle_status = gradle_cmd.status();
724        if gradle_status.is_err() || !gradle_status.unwrap().success() {
725            println!("āŒ Gradle build assembleDebug gagal.");
726            return;
727        }
728
729        // 6. adb install
730        println!("šŸ”Ø Memasang APK ke perangkat {} ({})...", device_name, device_id);
731        let install_status = std::process::Command::new("adb")
732            .args(["-s", &device_id, "install", "-r", "native/android/app/build/outputs/apk/debug/app-debug.apk"])
733            .status();
734
735        if install_status.is_err() || !install_status.unwrap().success() {
736            println!("āŒ Gagal memasang APK ke device.");
737            return;
738        }
739
740        // 7. adb reverse
741        let vite_port = "5173"; // default
742        let reverse_status = std::process::Command::new("adb")
743            .args(["-s", &device_id, "reverse", &format!("tcp:{}", vite_port), &format!("tcp:{}", vite_port)])
744            .status();
745        if reverse_status.is_err() {
746            println!("āš ļø Warning: Gagal melakukan adb reverse port {}", vite_port);
747        }
748
749        // 8. adb shell am start
750        println!("šŸš€ Membuka aplikasi di perangkat {}...", device_name);
751        let _ = std::process::Command::new("adb")
752            .args(["-s", &device_id, "logcat", "-c"])
753            .status();
754        
755        let _ = std::process::Command::new("adb")
756            .args(["-s", &device_id, "shell", "am", "start", "-n", "com.rustbasic.mobile/com.rustbasic.mobile.MainActivity"])
757            .status();
758
759        println!("šŸ“‹ Menampilkan log realtime dari perangkat {} (Tekan Ctrl+C untuk keluar)...", device_name);
760        let mut logcat_cmd = std::process::Command::new("adb");
761        logcat_cmd.args(["-s", &device_id, "logcat", "-s", "RustBasicServer"]);
762        let mut child = logcat_cmd.spawn().expect("Gagal menjalankan adb logcat");
763        let _ = child.wait();
764    } else if run_desktop {
765        println!("šŸš€ Memulai RustBasic Desktop Wrapper...");
766        let mut cmd = std::process::Command::new("cargo");
767        cmd.args(["run", "--bin", "rustbasic-desktop", "--features", "desktop"]);
768        let status = cmd.status();
769        match status {
770            Ok(s) if s.success() => {}
771            _ => {
772                println!("āŒ Gagal menjalankan Desktop Wrapper.");
773            }
774        }
775    }
776}
777
778fn get_adb_devices() -> Vec<(String, String)> {
779    let output = std::process::Command::new("adb").arg("devices").output();
780    let mut devices = Vec::new();
781    if let Ok(out) = output {
782        let stdout = String::from_utf8_lossy(&out.stdout);
783        for line in stdout.lines() {
784            if line.contains("device") && !line.contains("List of devices") {
785                let parts: Vec<&str> = line.split_whitespace().collect();
786                if parts.is_empty() { continue; }
787                let device_id = parts[0].to_string();
788                let model_out = std::process::Command::new("adb")
789                    .args(["-s", &device_id, "shell", "getprop", "ro.product.model"])
790                    .output();
791                let model = if let Ok(m_out) = model_out {
792                    String::from_utf8_lossy(&m_out.stdout).trim().to_string()
793                } else {
794                    "Unknown Device".to_string()
795                };
796                devices.push((device_id, model));
797            }
798        }
799    }
800    devices
801}
802
803fn compile_jni_libraries() -> bool {
804    println!("šŸš€ Building Rust library for Android (Native Rust implementation)...");
805
806    let _ = std::process::Command::new("rustup")
807        .args(["target", "add", "aarch64-linux-android", "armv7-linux-androideabi", "x86_64-linux-android"])
808        .status();
809
810    let home = std::env::var("HOME").unwrap_or_default();
811    let android_ndk_home = if let Ok(val) = std::env::var("ANDROID_NDK_HOME") {
812        val
813    } else {
814        let mac_ndk = format!("{}/Library/Android/sdk/ndk", home);
815        if std::path::Path::new(&mac_ndk).exists() {
816            if let Ok(entries) = std::fs::read_dir(&mac_ndk) {
817                let mut paths: Vec<_> = entries.flatten().map(|e| e.path()).collect();
818                paths.sort();
819                if let Some(highest) = paths.last() {
820                    highest.display().to_string()
821                } else {
822                    "".to_string()
823                }
824            } else {
825                "".to_string()
826            }
827        } else {
828            "".to_string()
829        }
830    };
831
832    if android_ndk_home.is_empty() {
833        println!("āŒ Error: ANDROID_NDK_HOME is not set. Please set ANDROID_NDK_HOME.");
834        return false;
835    }
836
837    println!("Using NDK: {}", android_ndk_home);
838
839    let os = std::env::consts::OS;
840    let toolchain_sub = if os == "macos" { "darwin-x86_64" } else { "linux-x86_64" };
841    let toolchain_bin_path = std::path::Path::new(&android_ndk_home)
842        .join("toolchains/llvm/prebuilt")
843        .join(toolchain_sub)
844        .join("bin");
845
846    if !toolchain_bin_path.exists() {
847        println!("āŒ Error: Toolchain bin path not found: {}", toolchain_bin_path.display());
848        return false;
849    }
850
851    let sqlite_version = "3450100";
852    let sqlite_dir = format!("target/sqlite-amalgamation-{}", sqlite_version);
853    if !std::path::Path::new(&sqlite_dir).exists() {
854        println!("šŸ“„ Downloading SQLite source amalgamation...");
855        std::fs::create_dir_all("target").ok();
856        
857        let zip_path = "target/sqlite.zip";
858        let sqlite_url = format!("https://www.sqlite.org/2024/sqlite-amalgamation-{}.zip", sqlite_version);
859        
860        let curl_status = std::process::Command::new("curl")
861            .args(["-sSLo", zip_path, &sqlite_url])
862            .status();
863        
864        if curl_status.is_err() || !curl_status.unwrap().success() {
865            println!("āŒ Gagal men-download SQLite source.");
866            return false;
867        }
868
869        let unzip_status = std::process::Command::new("unzip")
870            .args(["-q", zip_path, "-d", "target/"])
871            .status();
872
873        let _ = std::fs::remove_file(zip_path);
874
875        if unzip_status.is_err() || !unzip_status.unwrap().success() {
876            println!("āŒ Gagal mengekstrak SQLite source.");
877            return false;
878        }
879    }
880
881    let targets = vec![
882        ("aarch64-linux-android", "arm64-v8a", "aarch64-linux-android21-clang"),
883        ("armv7-linux-androideabi", "armeabi-v7a", "armv7a-linux-androideabi21-clang"),
884        ("x86_64-linux-android", "x86_64", "x86_64-linux-android21-clang"),
885    ];
886
887    let jnilibs_dir = "native/android/app/src/main/jniLibs";
888
889    for (target, arch, clang_name) in targets {
890        println!("šŸ”Ø Preparing SQLite static library for {}...", target);
891        
892        let clang_path = toolchain_bin_path.join(clang_name);
893        let ar_path = toolchain_bin_path.join("llvm-ar");
894
895        if !clang_path.exists() {
896            println!("āŒ Error: Compiler not found: {}", clang_path.display());
897            return false;
898        }
899
900        let sqlite_out = format!("target/{}/sqlite", target);
901        std::fs::create_dir_all(&sqlite_out).ok();
902
903        let libsqlite3_a = format!("{}/libsqlite3.a", sqlite_out);
904        if !std::path::Path::new(&libsqlite3_a).exists() {
905            println!("   Compiling SQLite static lib for {}...", target);
906            let sqlite3_o = format!("{}/sqlite3.o", sqlite_out);
907            let sqlite3_c = format!("{}/sqlite3.c", sqlite_dir);
908            
909            let compile_status = std::process::Command::new(&clang_path)
910                .args(["-O2", "-c", &sqlite3_c, "-o", &sqlite3_o])
911                .status();
912
913            if compile_status.is_err() || !compile_status.unwrap().success() {
914                println!("āŒ Gagal mengompilasi sqlite3.o");
915                return false;
916            }
917
918            let archive_status = std::process::Command::new(&ar_path)
919                .args(["rcs", &libsqlite3_a, &sqlite3_o])
920                .status();
921
922            if archive_status.is_err() || !archive_status.unwrap().success() {
923                println!("āŒ Gagal mengarsip libsqlite3.a");
924                return false;
925            }
926        }
927
928        println!("šŸ”Ø Compiling Rust library for {}...", target);
929        let mut cargo_cmd = std::process::Command::new("cargo");
930        cargo_cmd.args(["build", "--target", target, "--release"]);
931
932        let clang_path_str = clang_path.display().to_string();
933        let ar_path_str = ar_path.display().to_string();
934
935        let target_upper = target.replace("-", "_").to_uppercase();
936        let linker_env = format!("CARGO_TARGET_{}_LINKER", target_upper);
937        let cc_env = format!("CC_{}", target.replace("-", "_"));
938        let ar_env = format!("AR_{}", target.replace("-", "_"));
939
940        cargo_cmd.env(&linker_env, &clang_path_str);
941        cargo_cmd.env(&cc_env, &clang_path_str);
942        cargo_cmd.env(&ar_env, &ar_path_str);
943
944        let cargo_status = cargo_cmd.status();
945        if cargo_status.is_err() || !cargo_status.unwrap().success() {
946            println!("āŒ Gagal mengompilasi library Rust untuk target {}", target);
947            return false;
948        }
949
950        let dest_dir = format!("{}/{}", jnilibs_dir, arch);
951        std::fs::create_dir_all(&dest_dir).ok();
952
953        let src_so = format!("target/{}/release/librustbasic.so", target);
954        let dest_so = format!("{}/librustbasic_mobile.so", dest_dir);
955
956        if let Err(e) = std::fs::copy(&src_so, &dest_so) {
957            println!("āŒ Gagal menyalin {}: {}", src_so, e);
958            return false;
959        }
960    }
961
962    println!("āœ… Android JNI libraries built successfully!");
963    true
964}
965
966// ============================================================
967// BUILD
968// ============================================================
969fn prompt_choice(prompt: &str, min: usize, max: usize) -> usize {
970    use std::io::{self, Write};
971    loop {
972        print!("{}", prompt);
973        let _ = io::stdout().flush();
974        let mut input = String::new();
975        if io::stdin().read_line(&mut input).is_ok() {
976            if let Ok(choice) = input.trim().parse::<usize>() {
977                if choice >= min && choice <= max {
978                    return choice;
979                }
980            }
981        }
982        println!("āš ļø Pilihan tidak valid, silakan coba lagi.");
983    }
984}
985
986pub async fn handle_build(args: &[String]) {
987    let mut build_docker  = args.iter().any(|a| a == "--docker");
988    let mut build_desktop = args.iter().any(|a| a == "--desktop");
989    let mut build_android = args.iter().any(|a| a == "--android");
990    let release_mode  = args.iter().any(|a| a == "--release" || a == "-r");
991    let mut target_type   = String::new();
992    let mut docker_tag    = String::new();
993    let mut docker_platform = String::new();
994
995    for i in 0..args.len() {
996        if args[i] == "--type" && i + 1 < args.len() { target_type = args[i+1].to_lowercase(); }
997        if args[i] == "--tag"  && i + 1 < args.len() { docker_tag  = args[i+1].clone(); }
998        if args[i] == "--platform" && i + 1 < args.len() { docker_platform = args[i+1].clone(); }
999    }
1000
1001    if !build_docker && !build_desktop && !build_android {
1002        let is_native_installed = if let Ok(content) = std::fs::read_to_string(".rustbasic_packages.json") {
1003            content.contains("\"rustbasic-native\"")
1004        } else {
1005            false
1006        };
1007
1008        println!("šŸ› ļø  RustBasic Build CLI");
1009        println!("Pilih platform target untuk di-build:");
1010        println!("  [1] Docker (Container Image)");
1011
1012        let max_choice = if is_native_installed {
1013            println!("  [2] Desktop Wrapper (Windows, macOS, Linux)");
1014            println!("  [3] Android Wrapper (APK, AAB)");
1015            3
1016        } else {
1017            1
1018        };
1019
1020        let prompt_str = format!("šŸ‘‰ Pilih nomor platform (1-{}): ", max_choice);
1021        match prompt_choice(&prompt_str, 1, max_choice) {
1022            1 => build_docker  = true,
1023            2 => build_desktop = true,
1024            3 => build_android = true,
1025            _ => {}
1026        }
1027    }
1028
1029    if build_docker {
1030        if docker_platform.is_empty() {
1031            println!("\nPilih Platform Target CPU Docker:");
1032            println!("  [1] Current Host Platform (Sesuai OS komputer Anda)");
1033            println!("  [2] Linux AMD64 / x86_64 (Standard VPS Intel/AMD - Umum/Rekomendasi)");
1034            println!("  [3] Linux ARM64 / aarch64 (Server berbasis ARM / AWS Graviton)");
1035            match prompt_choice("šŸ‘‰ Pilih (1-3): ", 1, 3) {
1036                2 => docker_platform = "linux/amd64".to_string(),
1037                3 => docker_platform = "linux/arm64".to_string(),
1038                _ => {}
1039            }
1040        }
1041        build_docker_image(&docker_tag, &docker_platform).await;
1042    } else if build_desktop {
1043        build_desktop_binary(args, release_mode).await;
1044    } else if build_android {
1045        build_android_apk(args, &target_type, release_mode).await;
1046    }
1047}
1048
1049async fn build_docker_image(custom_tag: &str, platform: &str) {
1050    // Cek Docker tersedia
1051    if !std::process::Command::new("docker").arg("version")
1052        .stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null())
1053        .status().map(|s| s.success()).unwrap_or(false)
1054    {
1055        println!("āŒ Docker tidak ditemukan. Install: https://docs.docker.com/get-docker/");
1056        return;
1057    }
1058
1059    let dockerfile_path = std::path::Path::new("Dockerfile");
1060    if !dockerfile_path.exists() {
1061        println!("šŸ“ Membuat Dockerfile...");
1062        let is_monorepo = std::path::Path::new("../rustbasic-core").exists() || std::path::Path::new("rustbasic-core").exists();
1063        let binary_name = get_cargo_package_name();
1064
1065        let dockerfile_content = if is_monorepo {
1066            r#"# ============================================================
1067# RustBasic Docker Build — Multi-stage
1068# ============================================================
1069
1070# Stage 1: Builder
1071FROM rust:1-slim-bookworm AS builder
1072
1073RUN apt-get update && apt-get install -y \
1074    pkg-config libssl-dev \
1075    && rm -rf /var/lib/apt/lists/*
1076
1077WORKDIR /build
1078
1079# Copy rustbasic-core (dari konteks workspace root)
1080COPY rustbasic-core /build/rustbasic-core
1081
1082# Copy proyek utama rustbasic
1083COPY rustbasic /build/rustbasic
1084
1085WORKDIR /build/rustbasic
1086
1087# Build release binary
1088RUN cargo build --release --bin rustbasic
1089
1090# Stage 2: Runtime
1091FROM debian:bookworm-slim
1092
1093RUN apt-get update && apt-get install -y \
1094    ca-certificates libssl3 \
1095    && rm -rf /var/lib/apt/lists/*
1096
1097WORKDIR /app
1098
1099# Copy binary dari builder stage
1100COPY --from=builder /build/rustbasic/target/release/rustbasic .
1101
1102# Copy assets yang diperlukan dari builder stage (lebih aman dan bersih)
1103COPY --from=builder /build/rustbasic/src/resources/views/ src/resources/views/
1104COPY --from=builder /build/rustbasic/src/dist/ src/dist/
1105COPY --from=builder /build/public/ public/
1106COPY --from=builder /build/database/migrations/ database/migrations/
1107COPY --from=builder /build/database/seeders/ database/seeders/
1108COPY --from=builder /build/.env.example .env
1109
1110# Expose port aplikasi
1111EXPOSE 4000
1112
1113CMD ["./rustbasic"]
1114"#.to_string()
1115        } else {
1116            format!(r#"# ============================================================
1117# RustBasic Docker Build — Multi-stage
1118# ============================================================
1119
1120# Stage 1: Builder
1121FROM rust:1-slim-bookworm AS builder
1122
1123RUN apt-get update && apt-get install -y \
1124    pkg-config libssl-dev \
1125    && rm -rf /var/lib/apt/lists/*
1126
1127WORKDIR /build
1128
1129# Copy proyek utama
1130COPY . .
1131
1132# Build release binary
1133RUN cargo build --release --bin {bin_name}
1134
1135# Stage 2: Runtime
1136FROM debian:bookworm-slim
1137
1138RUN apt-get update && apt-get install -y \
1139    ca-certificates libssl3 \
1140    && rm -rf /var/lib/apt/lists/*
1141
1142WORKDIR /app
1143
1144# Copy binary dari builder stage
1145COPY --from=builder /build/target/release/{bin_name} .
1146
1147# Copy assets yang diperlukan dari builder stage
1148COPY --from=builder /build/src/resources/views/ src/resources/views/
1149COPY --from=builder /build/src/dist/ src/dist/
1150COPY --from=builder /build/public/ public/
1151COPY --from=builder /build/database/migrations/ database/migrations/
1152COPY --from=builder /build/database/seeders/ database/seeders/
1153COPY --from=builder /build/.env.example .env
1154
1155# Expose port aplikasi
1156EXPOSE 4000
1157
1158CMD ["./{bin_name}"]
1159"#, bin_name = binary_name)
1160        };
1161
1162        if let Err(e) = std::fs::write(dockerfile_path, dockerfile_content) {
1163            println!("āŒ Gagal membuat Dockerfile: {}", e);
1164            return;
1165        }
1166        println!("āœ… Dockerfile berhasil dibuat.");
1167    }
1168
1169    let app_name = std::env::var("APP_NAME").unwrap_or_else(|_| get_cargo_package_name()).to_lowercase();
1170    let image_tag = if custom_tag.is_empty() { format!("{}:latest", app_name) } else { custom_tag.to_string() };
1171
1172    if std::path::Path::new("package.json").exists() {
1173        println!("šŸ“¦ Mengompilasi frontend assets (npm run build)...");
1174        let npm_cmd = if cfg!(windows) { "npm.cmd" } else { "npm" };
1175        let success = match std::process::Command::new(npm_cmd)
1176            .args(&["run", "build"])
1177            .status()
1178        {
1179            Ok(status) => status.success(),
1180            Err(_) => {
1181                println!("āŒ Gagal menjalankan npm. Pastikan Node.js & npm terinstal di sistem Anda.");
1182                return;
1183            }
1184        };
1185
1186        if !success {
1187            println!("āŒ Gagal mengompilasi frontend assets. Silakan periksa error di atas.");
1188            return;
1189        }
1190    }
1191
1192    println!("\n🐳 Docker build dimulai...");
1193    println!("   Image: {}", image_tag);
1194    
1195    let core_context = if std::path::Path::new("../rustbasic-core").exists() {
1196        "core=../rustbasic-core".to_string()
1197    } else {
1198        "core=.".to_string()
1199    };
1200
1201    let mut build_args = vec![
1202        "build".to_string(),
1203        "--build-context".to_string(),
1204        core_context,
1205    ];
1206    
1207    if !platform.is_empty() {
1208        build_args.push("--platform".to_string());
1209        build_args.push(platform.to_string());
1210        println!("   Platform: {}", platform);
1211    }
1212    
1213    build_args.push("-t".to_string());
1214    build_args.push(image_tag.clone());
1215    build_args.push(".".to_string());
1216
1217    println!("   Running: docker {}", build_args.join(" "));
1218
1219    let mut cmd = std::process::Command::new("docker")
1220        .args(&build_args)
1221        .stdin(std::process::Stdio::inherit())
1222        .stdout(std::process::Stdio::inherit())
1223        .stderr(std::process::Stdio::inherit())
1224        .spawn().expect("Gagal menjalankan docker build");
1225
1226    let success = cmd.wait().map(|s| s.success()).unwrap_or(false);
1227
1228    if success {
1229        println!("\nāœ… Docker build selesai! Image: {}", image_tag);
1230        println!("   Jalankan container (Lokal/Development):");
1231        if !platform.is_empty() {
1232            println!("   docker run --platform {} -p 4000:4000 --env-file .env {}", platform, image_tag);
1233        } else {
1234            println!("   docker run -p 4000:4000 --env-file .env {}", image_tag);
1235        }
1236        println!("   Jalankan container (Produksi/Server - Auto Restart):");
1237        if !platform.is_empty() {
1238            println!("   docker run --platform {} -d -p 80:4000 --restart unless-stopped --env-file .env {}", platform, image_tag);
1239        } else {
1240            println!("   docker run -d -p 80:4000 --restart unless-stopped --env-file .env {}", image_tag);
1241        }
1242    } else {
1243        println!("\nāŒ Docker build gagal.");
1244    }
1245}
1246
1247async fn build_desktop_binary(args: &[String], mut release_mode: bool) {
1248    let mut target_triple = "";
1249
1250    for i in 0..args.len() {
1251        if args[i] == "--os" && i + 1 < args.len() {
1252            target_triple = match args[i+1].as_str() {
1253                "macos-intel"   => "x86_64-apple-darwin",
1254                "macos-silicon" => "aarch64-apple-darwin",
1255                "windows"       => "x86_64-pc-windows-msvc",
1256                "windows-gnu"   => "x86_64-pc-windows-gnu",
1257                "linux"         => "x86_64-unknown-linux-gnu",
1258                _               => "",
1259            };
1260        }
1261    }
1262
1263    if target_triple.is_empty() && !args.iter().any(|a| a.starts_with("--os")) {
1264        println!("\nPilih OS Target Desktop:");
1265        println!("  [1] Current OS");
1266        println!("  [2] macOS Intel (x86_64-apple-darwin)");
1267        println!("  [3] macOS Apple Silicon (aarch64-apple-darwin)");
1268        println!("  [4] Windows MSVC (x86_64-pc-windows-msvc)");
1269        println!("  [5] Windows GNU (x86_64-pc-windows-gnu - Rekomendasi Cross-compile dari macOS/Linux)");
1270        println!("  [6] Linux (x86_64-unknown-linux-gnu)");
1271        match prompt_choice("šŸ‘‰ Pilih (1-6): ", 1, 6) {
1272            2 => target_triple = "x86_64-apple-darwin",
1273            3 => target_triple = "aarch64-apple-darwin",
1274            4 => target_triple = "x86_64-pc-windows-msvc",
1275            5 => target_triple = "x86_64-pc-windows-gnu",
1276            6 => target_triple = "x86_64-unknown-linux-gnu",
1277            _ => {}
1278        }
1279    }
1280
1281    if !args.iter().any(|a| a == "--release" || a == "-r" || a == "--debug" || a == "-d") {
1282        println!("\n  [1] Debug\n  [2] Release");
1283        if prompt_choice("šŸ‘‰ Mode (1-2): ", 1, 2) == 2 { release_mode = true; }
1284    }
1285
1286    let mut build_args = vec!["build", "--bin", "rustbasic-desktop", "--features", "desktop"];
1287    if release_mode   { build_args.push("--release"); }
1288    if !target_triple.is_empty() {
1289        build_args.push("--target");
1290        build_args.push(target_triple);
1291        let _ = std::process::Command::new("rustup").args(["target", "add", target_triple]).status();
1292    }
1293
1294    println!("\nšŸ–„ļø  Desktop build: cargo {}", build_args.join(" "));
1295    let mut cmd = std::process::Command::new("cargo")
1296        .args(&build_args)
1297        .stdin(std::process::Stdio::inherit())
1298        .stdout(std::process::Stdio::inherit())
1299        .stderr(std::process::Stdio::inherit())
1300        .spawn().expect("Gagal menjalankan cargo build");
1301
1302    if cmd.wait().map(|s| s.success()).unwrap_or(false) {
1303        println!("\nāœ… Desktop build selesai!");
1304    } else {
1305        println!("\nāŒ Desktop build gagal.");
1306    }
1307}
1308
1309async fn build_android_apk(args: &[String], target_type: &str, mut release_mode: bool) {
1310    let is_aab = if target_type.is_empty() {
1311        println!("\n  [1] APK\n  [2] AAB (Google Play)");
1312        prompt_choice("šŸ‘‰ Format (1-2): ", 1, 2) == 2
1313    } else {
1314        target_type == "aab"
1315    };
1316
1317    if !args.iter().any(|a| a == "--release" || a == "-r" || a == "--debug" || a == "-d") {
1318        println!("\n  [1] Debug\n  [2] Release");
1319        if prompt_choice("šŸ‘‰ Mode (1-2): ", 1, 2) == 2 { release_mode = true; }
1320    }
1321
1322    // Build JNI
1323    println!("\nšŸ”Ø Membangun JNI library (Native implementation)...");
1324    if !compile_jni_libraries() {
1325        println!("āŒ JNI build gagal.");
1326        return;
1327    }
1328
1329    // Setup environment
1330    let home = std::env::var("HOME").unwrap_or_default();
1331    let android_home = std::env::var("ANDROID_HOME")
1332        .unwrap_or_else(|_| format!("{}/Library/Android/sdk", home));
1333    unsafe { std::env::set_var("ANDROID_HOME", &android_home); }
1334
1335    setup_java_home();
1336
1337    let gradle_task = match (is_aab, release_mode) {
1338        (false, false) => "assembleDebug",
1339        (false, true)  => "assembleRelease",
1340        (true,  false) => "bundleDebug",
1341        (true,  true)  => "bundleRelease",
1342    };
1343
1344    println!("\nšŸ”Ø Gradle task: {}", gradle_task);
1345    let gradlew = if std::path::Path::new("native/android/gradlew").exists() { "./gradlew" } else { "gradle" };
1346    let mut cmd = std::process::Command::new(gradlew);
1347    cmd.arg(gradle_task)
1348        .current_dir("native/android");
1349
1350    if let Ok(jh) = std::env::var("JAVA_HOME") {
1351        cmd.env("JAVA_HOME", jh);
1352    }
1353
1354    cmd.stdin(std::process::Stdio::inherit()).stdout(std::process::Stdio::inherit()).stderr(std::process::Stdio::inherit());
1355    let mut child = cmd.spawn().expect("Gagal menjalankan Gradle");
1356
1357    if child.wait().map(|s| s.success()).unwrap_or(false) {
1358        println!("\nāœ… Android build selesai!");
1359    } else {
1360        println!("\nāŒ Android build gagal.");
1361    }
1362}
1363
1364fn prompt_string(prompt: &str, default: &str) -> String {
1365    use std::io::Write;
1366    print!("{}", prompt);
1367    let _ = std::io::stdout().flush();
1368    let mut input = String::new();
1369    if std::io::stdin().read_line(&mut input).is_ok() {
1370        let trimmed = input.trim();
1371        if trimmed.is_empty() {
1372            default.to_string()
1373        } else {
1374            trimmed.to_string()
1375        }
1376    } else {
1377        default.to_string()
1378    }
1379}
1380
1381pub async fn handle_deploy() {
1382    println!("\n{}", "šŸš€ RustBasic Docker Deploy CLI".magenta().bold());
1383    println!("{}", "------------------------------".magenta());
1384
1385    // 1. Konfigurasi Image & Pengiriman
1386    let image_name = prompt_string("šŸ‘‰ Masukkan Nama/Tag Docker Image (default: rustbasic:latest): ", "rustbasic:latest");
1387
1388    // Cek apakah Docker image sudah ada
1389    let inspect = std::process::Command::new("docker")
1390        .args(["image", "inspect", &image_name])
1391        .stdout(std::process::Stdio::null())
1392        .stderr(std::process::Stdio::null())
1393        .status();
1394
1395    match inspect {
1396        Ok(status) if status.success() => {}
1397        _ => {
1398            println!("{}", format!("āš ļø  Peringatan: Image '{}' tidak ditemukan lokal.", image_name).yellow());
1399            let proceed = prompt_string("šŸ‘‰ Tetap lanjutkan proses ekspor? (y/n) [default: n]: ", "n");
1400            if proceed.to_lowercase() != "y" {
1401                println!("āŒ Proses dihentikan.");
1402                return;
1403            }
1404        }
1405    }
1406
1407    // 2. Ekspor ke tar
1408    println!("šŸ“¦ Mengekspor Docker image '{}' ke 'rustbasic.tar'...", image_name);
1409    let save_status = std::process::Command::new("docker")
1410        .args(["save", "-o", "rustbasic.tar", &image_name])
1411        .status();
1412
1413    match save_status {
1414        Ok(status) if status.success() => {
1415            println!("{}", "āœ… Image berhasil diekspor ke rustbasic.tar.".green());
1416        }
1417        _ => {
1418            println!("{}", "āŒ Gagal mengekspor Docker image.".red().bold());
1419            return;
1420        }
1421    }
1422
1423    // 3. Konfigurasi Pengiriman
1424    println!("\n{}", "🌐 Konfigurasi Pengiriman ke Server".cyan().bold());
1425    println!("{}", "-----------------------------------".cyan());
1426    let ssh_user = prompt_string("šŸ‘‰ Masukkan SSH Username Server (contoh: root) [default: root]: ", "root");
1427    let ssh_ip = prompt_string("šŸ‘‰ Masukkan IP Address Server: ", "");
1428    if ssh_ip.is_empty() {
1429        println!("{}", "āŒ IP Address server tidak boleh kosong.".red().bold());
1430        let _ = std::fs::remove_file("rustbasic.tar");
1431        return;
1432    }
1433    let ssh_port = prompt_string("šŸ‘‰ Masukkan SSH Port Server (default: 22): ", "22");
1434    let dest_dir = prompt_string("šŸ‘‰ Masukkan Folder Tujuan di Server (default: ~/app): ", "~/app");
1435    let server_port = prompt_string("šŸ‘‰ Masukkan Port Server Mapping (contoh: 80:4000) [default: 80:4000]: ", "80:4000");
1436    let env_file = prompt_string("šŸ‘‰ Masukkan File Env yang akan dikirim (default: .env): ", ".env");
1437
1438    println!("\nšŸš€ Menyiapkan folder tujuan di server...");
1439    let mkdir_status = std::process::Command::new("ssh")
1440        .args([
1441            "-p", &ssh_port,
1442            &format!("{}@{}", ssh_user, ssh_ip),
1443            &format!("mkdir -p {}", dest_dir)
1444        ])
1445        .status();
1446
1447    match mkdir_status {
1448        Ok(status) if status.success() => {}
1449        _ => {
1450            println!("{}", "āŒ Gagal terhubung ke server menggunakan SSH.".red().bold());
1451            let _ = std::fs::remove_file("rustbasic.tar");
1452            return;
1453        }
1454    }
1455
1456    println!("šŸš€ Mengirimkan berkas rustbasic.tar & {} ke server...", env_file);
1457    let scp_status = std::process::Command::new("scp")
1458        .args([
1459            "-P", &ssh_port,
1460            "rustbasic.tar", &env_file,
1461            &format!("{}@{}:{}", ssh_user, ssh_ip, dest_dir)
1462        ])
1463        .status();
1464
1465    // Hapus file tar lokal
1466    let _ = std::fs::remove_file("rustbasic.tar");
1467
1468    if let Ok(status) = scp_status {
1469        if !status.success() {
1470            println!("{}", "āŒ Gagal mengirimkan berkas via SCP.".red().bold());
1471            return;
1472        }
1473    } else {
1474        println!("{}", "āŒ Gagal menjalankan SCP.".red().bold());
1475        return;
1476    }
1477
1478    println!("{}", "āœ… Pengiriman berkas berhasil!".green());
1479
1480    // 4. Eksekusi SSH otomatis di server jika disetujui
1481    let auto_run = prompt_string("\nšŸ‘‰ Apakah Anda ingin langsung menjalankan container di server secara otomatis? (y/n) [default: y]: ", "y");
1482    if auto_run.to_lowercase() == "y" || auto_run.is_empty() {
1483        println!("\nšŸš€ Memuat image di server (docker load)...");
1484        let load_status = std::process::Command::new("ssh")
1485            .args([
1486                "-p", &ssh_port,
1487                &format!("{}@{}", ssh_user, ssh_ip),
1488                &format!("docker load -i {}/rustbasic.tar", dest_dir)
1489            ])
1490            .status();
1491
1492        match load_status {
1493            Ok(status) if status.success() => {
1494                println!("{}", "āœ… Image berhasil dimuat di server.".green());
1495            }
1496            _ => {
1497                println!("{}", "āŒ Gagal memuat image di server.".red().bold());
1498                return;
1499            }
1500        }
1501
1502        println!("šŸš€ Menghentikan & menghapus container lama 'rustbasic-app' jika ada...");
1503        let stop_status = std::process::Command::new("ssh")
1504            .args([
1505                "-p", &ssh_port,
1506                &format!("{}@{}", ssh_user, ssh_ip),
1507                "docker stop rustbasic-app || true && docker rm rustbasic-app || true"
1508            ])
1509            .status();
1510
1511        if let Err(e) = stop_status {
1512            println!("āš ļø Peringatan saat membersihkan container lama: {}", e);
1513        }
1514
1515        println!("šŸš€ Menjalankan container baru 'rustbasic-app'...");
1516        let run_cmd = format!(
1517            "docker run -d --name rustbasic-app -p {} --restart unless-stopped --env-file {}/.env {}",
1518            server_port, dest_dir, image_name
1519        );
1520        let run_status = std::process::Command::new("ssh")
1521            .args([
1522                "-p", &ssh_port,
1523                &format!("{}@{}", ssh_user, ssh_ip),
1524                &run_cmd
1525            ])
1526            .status();
1527
1528        match run_status {
1529            Ok(status) if status.success() => {
1530                println!("{}", "šŸŽ‰ Container 'rustbasic-app' berhasil dijalankan di server!".green().bold());
1531            }
1532            _ => {
1533                println!("{}", "āŒ Gagal menjalankan container di server.".red().bold());
1534                return;
1535            }
1536        }
1537
1538        println!("šŸš€ Membersihkan file tar di server...");
1539        let rm_status = std::process::Command::new("ssh")
1540            .args([
1541                "-p", &ssh_port,
1542                &format!("{}@{}", ssh_user, ssh_ip),
1543                &format!("rm {}/rustbasic.tar", dest_dir)
1544            ])
1545            .status();
1546
1547        if let Err(e) = rm_status {
1548            println!("āš ļø Peringatan saat membersihkan file tar di server: {}", e);
1549        }
1550
1551        println!("\n{}", "šŸŽ‰ Deployment selesai!".green().bold());
1552        println!("{}", "--------------------------------------------------------".green());
1553        println!("Untuk melihat log aplikasi di server, jalankan:");
1554        println!("ssh -p {} {}@{} \"docker logs -f rustbasic-app\"", ssh_port, ssh_user, ssh_ip);
1555        println!("{}", "--------------------------------------------------------".green());
1556    } else {
1557        println!("\n{}", "šŸ–„ļø  Langkah Selanjutnya di Server Anda:".cyan().bold());
1558        println!("{}", "--------------------------------------------------------".green());
1559        println!("1. Hubungkan ke server via SSH:");
1560        println!("   ssh -p {} {}@{}", ssh_port, ssh_user, ssh_ip);
1561        println!("");
1562        println!("2. Masuk ke folder tujuan:");
1563        println!("   cd {}", dest_dir);
1564        println!("");
1565        println!("3. Muat (load) image Docker dari berkas tar:");
1566        println!("   docker load -i rustbasic.tar");
1567        println!("");
1568        println!("4. Jalankan container dengan fitur auto-restart:");
1569        println!("   docker run -d --name rustbasic-app -p {} --restart unless-stopped --env-file .env {}", server_port, image_name);
1570        println!("");
1571        println!("5. Hapus file tar di server untuk menghemat disk:");
1572        println!("   rm rustbasic.tar");
1573        println!("{}", "--------------------------------------------------------".green());
1574    }
1575}
1576
1577// ============================================================
1578// PUBLISH
1579// ============================================================
1580fn handle_publish(target: &str) {
1581    let mut selected_target = target.to_string();
1582    if selected_target.is_empty() {
1583        println!("šŸ› ļø  RustBasic Configuration Publisher");
1584        println!("Pilih konfigurasi yang ingin dipublikasikan ke proyek Anda:");
1585        println!("  [1] CORS (Cross-Origin Resource Sharing)");
1586        println!("  [2] CSRF (Cross-Site Request Forgery)");
1587        println!("  [3] APP (Application settings & Storage path overrides)");
1588        let choice = prompt_choice("šŸ‘‰ Pilih nomor konfigurasi (1-3): ", 1, 3);
1589        match choice {
1590            1 => selected_target = "cors".to_string(),
1591            2 => selected_target = "csrf".to_string(),
1592            3 => selected_target = "app".to_string(),
1593            _ => return,
1594        }
1595    }
1596
1597    match selected_target.as_str() {
1598        "cors" => {
1599            let path = std::path::Path::new("src/config/cors.rs");
1600            if path.exists() {
1601                println!("ā„¹ļø  File cors.rs sudah ada di src/config/cors.rs");
1602                return;
1603            }
1604            if let Some(parent) = path.parent() {
1605                let _ = std::fs::create_dir_all(parent);
1606            }
1607            let content = r#"/* ---------------------------------------------------------
1608 * šŸ“‘ LABEL: CORS CONFIGURATION (src/config/cors.rs)
1609 * Berkas konfigurasi tambahan untuk kustomisasi CORS.
1610 * --------------------------------------------------------- */
1611
1612pub struct CorsConfig {
1613    pub allowed_origins: Vec<&'static str>,
1614    pub allowed_methods: Vec<&'static str>,
1615    pub allowed_headers: Vec<&'static str>,
1616}
1617
1618impl Default for CorsConfig {
1619    fn default() -> Self {
1620        Self {
1621            allowed_origins: vec!["*"],
1622            allowed_methods: vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"],
1623            allowed_headers: vec!["*"],
1624        }
1625    }
1626}
1627"#;
1628            if std::fs::write(path, content).is_ok() {
1629                println!("{} {}", "āœ… Berhasil mempublikasikan konfigurasi CORS ke:".green().bold(), path.display().to_string().cyan());
1630                println!("šŸ’” File ini sekarang dapat diimpor untuk menyesuaikan aturan CORS lokal.");
1631            } else {
1632                println!("āŒ Gagal menulis file CORS config.");
1633            }
1634        }
1635        "csrf" => {
1636            let path = std::path::Path::new("src/config/csrf.rs");
1637            if path.exists() {
1638                println!("ā„¹ļø  File csrf.rs sudah ada di src/config/csrf.rs");
1639                return;
1640            }
1641            if let Some(parent) = path.parent() {
1642                let _ = std::fs::create_dir_all(parent);
1643            }
1644            let content = r#"/* ---------------------------------------------------------
1645 * šŸ“‘ LABEL: CSRF CONFIGURATION (src/config/csrf.rs)
1646 * Berkas konfigurasi tambahan untuk perlindungan CSRF.
1647 * --------------------------------------------------------- */
1648
1649pub struct CsrfConfig {
1650    pub except_paths: Vec<&'static str>,
1651}
1652
1653impl Default for CsrfConfig {
1654    fn default() -> Self {
1655        Self {
1656            except_paths: vec![], // Masukkan rute yang dikecualikan dari CSRF di sini
1657        }
1658    }
1659}
1660"#;
1661            if std::fs::write(path, content).is_ok() {
1662                println!("{} {}", "āœ… Berhasil mempublikasikan konfigurasi CSRF ke:".green().bold(), path.display().to_string().cyan());
1663                println!("šŸ’” File ini sekarang dapat diimpor untuk mengecualikan rute tertentu dari CSRF.");
1664            } else {
1665                println!("āŒ Gagal menulis file CSRF config.");
1666            }
1667        }
1668        "app" => {
1669            let path = std::path::Path::new("src/config/app.rs");
1670            if path.exists() {
1671                println!("ā„¹ļø  File app.rs sudah ada di src/config/app.rs");
1672                return;
1673            }
1674            if let Some(parent) = path.parent() {
1675                let _ = std::fs::create_dir_all(parent);
1676            }
1677            let content = r#"/* ---------------------------------------------------------
1678 * šŸ“‘ LABEL: APP CONFIGURATION (src/config/app.rs)
1679 * Berkas konfigurasi tambahan untuk kustomisasi lokasi storage lokal.
1680 * --------------------------------------------------------- */
1681
1682pub const STORAGE_TARGET: &str = "public/storage";
1683pub const STORAGE_SOURCE: &str = "storage/app/public";
1684"#;
1685            if std::fs::write(path, content).is_ok() {
1686                println!("{} {}", "āœ… Berhasil mempublikasikan konfigurasi APP ke:".green().bold(), path.display().to_string().cyan());
1687            } else {
1688                println!("āŒ Gagal menulis file APP config.");
1689            }
1690        }
1691        _ => {
1692            println!("āŒ Target '{}' tidak dikenal untuk di-publish.", selected_target);
1693            println!("šŸ’” Target yang didukung: cors, csrf, app");
1694        }
1695    }
1696}
1697
1698fn get_cargo_package_name() -> String {
1699    if let Ok(content) = std::fs::read_to_string("Cargo.toml") {
1700        for line in content.lines() {
1701            let trimmed = line.trim();
1702            if trimmed.starts_with("name =") || trimmed.starts_with("name=") {
1703                let parts: Vec<&str> = trimmed.split('=').collect();
1704                if parts.len() > 1 {
1705                    let name = parts[1].trim().trim_matches('"').trim_matches('\'');
1706                    return name.to_string();
1707                }
1708            }
1709        }
1710    }
1711    "rustbasic".to_string()
1712}
1713