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        let mut extract = args.iter().any(|a| a == "--extract" || a == "-e");
1031        if docker_platform.is_empty() {
1032            println!("\nPilih Platform Target CPU Docker:");
1033            println!("  [1] Current Host Platform (Sesuai OS komputer Anda)");
1034            println!("  [2] Linux AMD64 / x86_64 (Standard VPS Intel/AMD - Umum/Rekomendasi)");
1035            println!("  [3] Linux ARM64 / aarch64 (Server berbasis ARM / AWS Graviton)");
1036            match prompt_choice("šŸ‘‰ Pilih (1-3): ", 1, 3) {
1037                2 => docker_platform = "linux/amd64".to_string(),
1038                3 => docker_platform = "linux/arm64".to_string(),
1039                _ => {}
1040            }
1041        }
1042
1043        // Peringatan Arsitektur CPU Mismatch
1044        let host_arch = std::env::consts::ARCH;
1045        let is_mismatch = (host_arch == "aarch64" && docker_platform == "linux/amd64")
1046            || (host_arch == "x86_64" && docker_platform == "linux/arm64");
1047
1048        if is_mismatch {
1049            println!("\nāš ļø  {}", "PERINGATAN: Arsitektur CPU Mismatch (Sangat Lambat)".yellow().bold());
1050            println!("   Anda berada di host dengan CPU '{}' tetapi memilih target Docker '{}'.", host_arch, docker_platform);
1051            println!("   Docker akan menggunakan emulasi CPU (QEMU) yang membuat proses kompilasi");
1052            println!("   Rust berjalan {} (bisa memakan waktu 10-30 menit).", "10x-20x LEBIH LAMBAT".red().bold());
1053            println!("   ");
1054            println!("   šŸ’” {} Kami telah mengaktifkan target caching untuk mempercepat build.", "TIPS:".cyan().bold());
1055            println!("      Build pertama tetap lambat, namun build berikutnya akan sangat cepat (1-2 menit)");
1056            println!("      karena target directory dan dependency cache disimpan oleh Docker.");
1057            println!("   ");
1058            let proceed = prompt_string("šŸ‘‰ Apakah Anda ingin melanjutkan proses build? (y/n) [default: y]: ", "y");
1059            if !proceed.to_lowercase().starts_with('y') {
1060                println!("āŒ Build dibatalkan oleh pengguna.");
1061                return;
1062            }
1063        }
1064
1065        if !extract && !args.iter().any(|a| a == "--docker") {
1066            let extract_choice = prompt_string("\nšŸ‘‰ Apakah Anda ingin mengekstrak biner Linux hasil build ke folder './build-output'? (y/n) [default: n]: ", "n");
1067            extract = extract_choice.to_lowercase().starts_with('y');
1068        }
1069
1070        build_docker_image(&docker_tag, &docker_platform, extract).await;
1071    } else if build_desktop {
1072        build_desktop_binary(args, release_mode).await;
1073    } else if build_android {
1074        build_android_apk(args, &target_type, release_mode).await;
1075    }
1076}
1077
1078async fn build_docker_image(custom_tag: &str, platform: &str, extract_binary: bool) {
1079    // Cek Docker tersedia
1080    if !std::process::Command::new("docker").arg("version")
1081        .stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null())
1082        .status().map(|s| s.success()).unwrap_or(false)
1083    {
1084        println!("āŒ Docker tidak ditemukan. Install: https://docs.docker.com/get-docker/");
1085        return;
1086    }
1087
1088    let dockerfile_path = std::path::Path::new("Dockerfile");
1089    if !dockerfile_path.exists() {
1090        println!("šŸ“ Membuat Dockerfile...");
1091        let is_monorepo = std::path::Path::new("../rustbasic-core").exists() || std::path::Path::new("rustbasic-core").exists();
1092        let binary_name = get_cargo_package_name();
1093
1094        let dockerfile_content = if is_monorepo {
1095            r#"# ============================================================
1096# RustBasic Docker Build — Standalone (Cached)
1097# ============================================================
1098
1099# Stage 1: Builder
1100FROM rust:1-slim-bookworm AS builder
1101
1102RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
1103    --mount=type=cache,target=/var/lib/apt,sharing=locked \
1104    apt-get update && apt-get install -y \
1105    pkg-config libssl-dev
1106
1107# Copy rustbasic-core (dari konteks workspace root)
1108WORKDIR /rustbasic-core
1109COPY --from=core . .
1110
1111# Copy proyek utama rustbasic
1112WORKDIR /build
1113COPY . .
1114
1115# Build release binary using Cargo registry, git cache, and target cache
1116ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
1117RUN --mount=type=cache,target=/usr/local/cargo/registry \
1118    --mount=type=cache,target=/usr/local/cargo/git \
1119    --mount=type=cache,target=/build/target \
1120    cargo build --release --bin rustbasic && \
1121    cp target/release/rustbasic /build/rustbasic-bin
1122
1123# Stage 2: Runtime
1124FROM debian:bookworm-slim
1125
1126RUN apt-get update && apt-get install -y \
1127    ca-certificates libssl3 \
1128    && rm -rf /var/lib/apt/lists/*
1129
1130WORKDIR /app
1131
1132# Copy binary dari builder stage
1133COPY --from=builder /build/rustbasic-bin ./rustbasic
1134
1135# Copy assets yang diperlukan dari builder stage (lebih aman dan bersih)
1136COPY --from=builder /build/src/resources/views/ src/resources/views/
1137COPY --from=builder /build/src/dist/ src/dist/
1138COPY --from=builder /build/public/ public/
1139COPY --from=builder /build/database/ database/
1140COPY --from=builder /build/.env.example .env
1141
1142# Expose port aplikasi
1143EXPOSE 4000
1144
1145CMD ["./rustbasic"]
1146"#.to_string()
1147        } else {
1148            format!(r#"# ============================================================
1149# RustBasic Docker Build — Standalone (Cached)
1150# ============================================================
1151
1152# Stage 1: Builder
1153FROM rust:1-slim-bookworm AS builder
1154
1155RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
1156    --mount=type=cache,target=/var/lib/apt,sharing=locked \
1157    apt-get update && apt-get install -y \
1158    pkg-config libssl-dev
1159
1160WORKDIR /build
1161
1162COPY . .
1163
1164# Build release binary using Cargo registry, git cache, and target cache
1165ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
1166RUN --mount=type=cache,target=/usr/local/cargo/registry \
1167    --mount=type=cache,target=/usr/local/cargo/git \
1168    --mount=type=cache,target=/build/target \
1169    cargo build --release --bin {bin_name} && \
1170    cp target/release/{bin_name} /build/{bin_name}-bin
1171
1172# Stage 2: Runtime
1173FROM debian:bookworm-slim
1174
1175RUN apt-get update && apt-get install -y \
1176    ca-certificates libssl3 \
1177    && rm -rf /var/lib/apt/lists/*
1178
1179WORKDIR /app
1180
1181# Copy binary dari builder stage
1182COPY --from=builder /build/{bin_name}-bin ./{bin_name}
1183
1184# Copy assets yang diperlukan dari builder stage
1185COPY --from=builder /build/src/resources/views/ src/resources/views/
1186COPY --from=builder /build/src/dist/ src/dist/
1187COPY --from=builder /build/public/ public/
1188COPY --from=builder /build/database/ database/
1189COPY --from=builder /build/.env.example .env
1190
1191# Expose port aplikasi
1192EXPOSE 4000
1193
1194CMD ["./{bin_name}"]
1195"#, bin_name = binary_name)
1196        };
1197
1198        if let Err(e) = std::fs::write(dockerfile_path, dockerfile_content) {
1199            println!("āŒ Gagal membuat Dockerfile: {}", e);
1200            return;
1201        }
1202        println!("āœ… Dockerfile berhasil dibuat.");
1203    }
1204
1205    let app_name = std::env::var("APP_NAME").unwrap_or_else(|_| get_cargo_package_name()).to_lowercase();
1206    let image_tag = if custom_tag.is_empty() { format!("{}:latest", app_name) } else { custom_tag.to_string() };
1207
1208    if std::path::Path::new("package.json").exists() {
1209        println!("šŸ“¦ Mengompilasi frontend assets (npm run build)...");
1210        let npm_cmd = if cfg!(windows) { "npm.cmd" } else { "npm" };
1211        let success = match std::process::Command::new(npm_cmd)
1212            .args(&["run", "build"])
1213            .status()
1214        {
1215            Ok(status) => status.success(),
1216            Err(_) => {
1217                println!("āŒ Gagal menjalankan npm. Pastikan Node.js & npm terinstal di sistem Anda.");
1218                return;
1219            }
1220        };
1221
1222        if !success {
1223            println!("āŒ Gagal mengompilasi frontend assets. Silakan periksa error di atas.");
1224            return;
1225        }
1226    }
1227
1228    println!("\n🐳 Docker build dimulai...");
1229    println!("   Image: {}", image_tag);
1230    
1231    let core_context = if std::path::Path::new("../rustbasic-core").exists() {
1232        "core=../rustbasic-core".to_string()
1233    } else {
1234        "core=.".to_string()
1235    };
1236
1237    let mut build_args = vec![
1238        "build".to_string(),
1239        "--build-context".to_string(),
1240        core_context,
1241    ];
1242    
1243    if !platform.is_empty() {
1244        build_args.push("--platform".to_string());
1245        build_args.push(platform.to_string());
1246        println!("   Platform: {}", platform);
1247    }
1248    
1249    build_args.push("-t".to_string());
1250    build_args.push(image_tag.clone());
1251    build_args.push(".".to_string());
1252
1253    println!("   Running: docker {}", build_args.join(" "));
1254
1255    let mut cmd = std::process::Command::new("docker")
1256        .args(&build_args)
1257        .stdin(std::process::Stdio::inherit())
1258        .stdout(std::process::Stdio::inherit())
1259        .stderr(std::process::Stdio::inherit())
1260        .spawn().expect("Gagal menjalankan docker build");
1261
1262    let success = cmd.wait().map(|s| s.success()).unwrap_or(false);
1263
1264    if success {
1265        println!("\nāœ… Docker build selesai! Image: {}", image_tag);
1266
1267        if extract_binary {
1268            println!("šŸ“¦ Mengekstrak biner Linux dari image Docker...");
1269            let container_name = format!("temp-extract-{}", std::time::SystemTime::now()
1270                .duration_since(std::time::UNIX_EPOCH)
1271                .unwrap_or_default()
1272                .as_secs());
1273            
1274            // docker create
1275            let create_res = std::process::Command::new("docker")
1276                .args(["create", "--name", &container_name, &image_tag])
1277                .status();
1278
1279            if create_res.is_ok() && create_res.unwrap().success() {
1280                // create build-output directory
1281                let _ = std::fs::create_dir_all("build-output");
1282                
1283                // Get cargo package name (which is the binary name inside container)
1284                let binary_name = get_cargo_package_name();
1285
1286                // docker cp
1287                let cp_status = std::process::Command::new("docker")
1288                    .args(["cp", &format!("{}:/app/{}", container_name, binary_name), &format!("build-output/{}", binary_name)])
1289                    .status();
1290                    
1291                // docker rm
1292                let _ = std::process::Command::new("docker")
1293                    .args(["rm", &container_name])
1294                    .status();
1295                    
1296                if cp_status.is_ok() && cp_status.unwrap().success() {
1297                    println!("āœ… Biner berhasil diekstrak ke: {}", format!("build-output/{}", binary_name).cyan().bold());
1298                } else {
1299                    println!("āŒ Gagal mengekstrak biner dari container.");
1300                }
1301            } else {
1302                println!("āŒ Gagal membuat temporary container untuk ekstraksi.");
1303            }
1304        }
1305
1306        println!("   Jalankan container (Lokal/Development):");
1307        if !platform.is_empty() {
1308            println!("   docker run --platform {} -p 4000:4000 --env-file .env {}", platform, image_tag);
1309        } else {
1310            println!("   docker run -p 4000:4000 --env-file .env {}", image_tag);
1311        }
1312        println!("   Jalankan container (Produksi/Server - Auto Restart):");
1313        if !platform.is_empty() {
1314            println!("   docker run --platform {} -d -p 80:4000 --restart unless-stopped --env-file .env {}", platform, image_tag);
1315        } else {
1316            println!("   docker run -d -p 80:4000 --restart unless-stopped --env-file .env {}", image_tag);
1317        }
1318    } else {
1319        println!("\nāŒ Docker build gagal.");
1320    }
1321}
1322
1323async fn build_desktop_binary(args: &[String], mut release_mode: bool) {
1324    let mut target_triple = "";
1325
1326    for i in 0..args.len() {
1327        if args[i] == "--os" && i + 1 < args.len() {
1328            target_triple = match args[i+1].as_str() {
1329                "macos-intel"   => "x86_64-apple-darwin",
1330                "macos-silicon" => "aarch64-apple-darwin",
1331                "windows"       => "x86_64-pc-windows-msvc",
1332                "windows-gnu"   => "x86_64-pc-windows-gnu",
1333                "linux"         => "x86_64-unknown-linux-gnu",
1334                _               => "",
1335            };
1336        }
1337    }
1338
1339    if target_triple.is_empty() && !args.iter().any(|a| a.starts_with("--os")) {
1340        println!("\nPilih OS Target Desktop:");
1341        println!("  [1] Current OS");
1342        println!("  [2] macOS Intel (x86_64-apple-darwin)");
1343        println!("  [3] macOS Apple Silicon (aarch64-apple-darwin)");
1344        println!("  [4] Windows MSVC (x86_64-pc-windows-msvc)");
1345        println!("  [5] Windows GNU (x86_64-pc-windows-gnu - Rekomendasi Cross-compile dari macOS/Linux)");
1346        println!("  [6] Linux (x86_64-unknown-linux-gnu)");
1347        match prompt_choice("šŸ‘‰ Pilih (1-6): ", 1, 6) {
1348            2 => target_triple = "x86_64-apple-darwin",
1349            3 => target_triple = "aarch64-apple-darwin",
1350            4 => target_triple = "x86_64-pc-windows-msvc",
1351            5 => target_triple = "x86_64-pc-windows-gnu",
1352            6 => target_triple = "x86_64-unknown-linux-gnu",
1353            _ => {}
1354        }
1355    }
1356
1357    if !args.iter().any(|a| a == "--release" || a == "-r" || a == "--debug" || a == "-d") {
1358        println!("\n  [1] Debug\n  [2] Release");
1359        if prompt_choice("šŸ‘‰ Mode (1-2): ", 1, 2) == 2 { release_mode = true; }
1360    }
1361
1362    let mut build_args = vec!["build", "--bin", "rustbasic-desktop", "--features", "desktop"];
1363    if release_mode   { build_args.push("--release"); }
1364    if !target_triple.is_empty() {
1365        build_args.push("--target");
1366        build_args.push(target_triple);
1367        let _ = std::process::Command::new("rustup").args(["target", "add", target_triple]).status();
1368    }
1369
1370    println!("\nšŸ–„ļø  Desktop build: cargo {}", build_args.join(" "));
1371    let mut cmd = std::process::Command::new("cargo")
1372        .args(&build_args)
1373        .stdin(std::process::Stdio::inherit())
1374        .stdout(std::process::Stdio::inherit())
1375        .stderr(std::process::Stdio::inherit())
1376        .spawn().expect("Gagal menjalankan cargo build");
1377
1378    if cmd.wait().map(|s| s.success()).unwrap_or(false) {
1379        println!("\nāœ… Desktop build selesai!");
1380    } else {
1381        println!("\nāŒ Desktop build gagal.");
1382    }
1383}
1384
1385async fn build_android_apk(args: &[String], target_type: &str, mut release_mode: bool) {
1386    let is_aab = if target_type.is_empty() {
1387        println!("\n  [1] APK\n  [2] AAB (Google Play)");
1388        prompt_choice("šŸ‘‰ Format (1-2): ", 1, 2) == 2
1389    } else {
1390        target_type == "aab"
1391    };
1392
1393    if !args.iter().any(|a| a == "--release" || a == "-r" || a == "--debug" || a == "-d") {
1394        println!("\n  [1] Debug\n  [2] Release");
1395        if prompt_choice("šŸ‘‰ Mode (1-2): ", 1, 2) == 2 { release_mode = true; }
1396    }
1397
1398    // Build JNI
1399    println!("\nšŸ”Ø Membangun JNI library (Native implementation)...");
1400    if !compile_jni_libraries() {
1401        println!("āŒ JNI build gagal.");
1402        return;
1403    }
1404
1405    // Setup environment
1406    let home = std::env::var("HOME").unwrap_or_default();
1407    let android_home = std::env::var("ANDROID_HOME")
1408        .unwrap_or_else(|_| format!("{}/Library/Android/sdk", home));
1409    unsafe { std::env::set_var("ANDROID_HOME", &android_home); }
1410
1411    setup_java_home();
1412
1413    let gradle_task = match (is_aab, release_mode) {
1414        (false, false) => "assembleDebug",
1415        (false, true)  => "assembleRelease",
1416        (true,  false) => "bundleDebug",
1417        (true,  true)  => "bundleRelease",
1418    };
1419
1420    println!("\nšŸ”Ø Gradle task: {}", gradle_task);
1421    let gradlew = if std::path::Path::new("native/android/gradlew").exists() { "./gradlew" } else { "gradle" };
1422    let mut cmd = std::process::Command::new(gradlew);
1423    cmd.arg(gradle_task)
1424        .current_dir("native/android");
1425
1426    if let Ok(jh) = std::env::var("JAVA_HOME") {
1427        cmd.env("JAVA_HOME", jh);
1428    }
1429
1430    cmd.stdin(std::process::Stdio::inherit()).stdout(std::process::Stdio::inherit()).stderr(std::process::Stdio::inherit());
1431    let mut child = cmd.spawn().expect("Gagal menjalankan Gradle");
1432
1433    if child.wait().map(|s| s.success()).unwrap_or(false) {
1434        println!("\nāœ… Android build selesai!");
1435    } else {
1436        println!("\nāŒ Android build gagal.");
1437    }
1438}
1439
1440fn prompt_string(prompt: &str, default: &str) -> String {
1441    use std::io::Write;
1442    print!("{}", prompt);
1443    let _ = std::io::stdout().flush();
1444    let mut input = String::new();
1445    if std::io::stdin().read_line(&mut input).is_ok() {
1446        let trimmed = input.trim();
1447        if trimmed.is_empty() {
1448            default.to_string()
1449        } else {
1450            trimmed.to_string()
1451        }
1452    } else {
1453        default.to_string()
1454    }
1455}
1456
1457pub async fn handle_deploy() {
1458    println!("\n{}", "šŸš€ RustBasic Docker Deploy CLI".magenta().bold());
1459    println!("{}", "------------------------------".magenta());
1460
1461    // 1. Konfigurasi Image & Pengiriman
1462    let image_name = prompt_string("šŸ‘‰ Masukkan Nama/Tag Docker Image (default: rustbasic:latest): ", "rustbasic:latest");
1463
1464    // Cek apakah Docker image sudah ada
1465    let inspect = std::process::Command::new("docker")
1466        .args(["image", "inspect", &image_name])
1467        .stdout(std::process::Stdio::null())
1468        .stderr(std::process::Stdio::null())
1469        .status();
1470
1471    match inspect {
1472        Ok(status) if status.success() => {}
1473        _ => {
1474            println!("{}", format!("āš ļø  Peringatan: Image '{}' tidak ditemukan lokal.", image_name).yellow());
1475            let proceed = prompt_string("šŸ‘‰ Tetap lanjutkan proses ekspor? (y/n) [default: n]: ", "n");
1476            if proceed.to_lowercase() != "y" {
1477                println!("āŒ Proses dihentikan.");
1478                return;
1479            }
1480        }
1481    }
1482
1483    // 2. Ekspor ke tar
1484    println!("šŸ“¦ Mengekspor Docker image '{}' ke 'rustbasic.tar'...", image_name);
1485    let save_status = std::process::Command::new("docker")
1486        .args(["save", "-o", "rustbasic.tar", &image_name])
1487        .status();
1488
1489    match save_status {
1490        Ok(status) if status.success() => {
1491            println!("{}", "āœ… Image berhasil diekspor ke rustbasic.tar.".green());
1492        }
1493        _ => {
1494            println!("{}", "āŒ Gagal mengekspor Docker image.".red().bold());
1495            return;
1496        }
1497    }
1498
1499    // 3. Konfigurasi Pengiriman
1500    println!("\n{}", "🌐 Konfigurasi Pengiriman ke Server".cyan().bold());
1501    println!("{}", "-----------------------------------".cyan());
1502    let ssh_user = prompt_string("šŸ‘‰ Masukkan SSH Username Server (contoh: root) [default: root]: ", "root");
1503    let ssh_ip = prompt_string("šŸ‘‰ Masukkan IP Address Server: ", "");
1504    if ssh_ip.is_empty() {
1505        println!("{}", "āŒ IP Address server tidak boleh kosong.".red().bold());
1506        let _ = std::fs::remove_file("rustbasic.tar");
1507        return;
1508    }
1509    let ssh_port = prompt_string("šŸ‘‰ Masukkan SSH Port Server (default: 22): ", "22");
1510    let dest_dir = prompt_string("šŸ‘‰ Masukkan Folder Tujuan di Server (default: ~/app): ", "~/app");
1511    let server_port = prompt_string("šŸ‘‰ Masukkan Port Server Mapping (contoh: 80:4000) [default: 80:4000]: ", "80:4000");
1512    let env_file = prompt_string("šŸ‘‰ Masukkan File Env yang akan dikirim (default: .env): ", ".env");
1513
1514    println!("\nšŸš€ Menyiapkan folder tujuan di server...");
1515    let mkdir_status = std::process::Command::new("ssh")
1516        .args([
1517            "-p", &ssh_port,
1518            &format!("{}@{}", ssh_user, ssh_ip),
1519            &format!("mkdir -p {}", dest_dir)
1520        ])
1521        .status();
1522
1523    match mkdir_status {
1524        Ok(status) if status.success() => {}
1525        _ => {
1526            println!("{}", "āŒ Gagal terhubung ke server menggunakan SSH.".red().bold());
1527            let _ = std::fs::remove_file("rustbasic.tar");
1528            return;
1529        }
1530    }
1531
1532    println!("šŸš€ Mengirimkan berkas rustbasic.tar & {} ke server...", env_file);
1533    let scp_status = std::process::Command::new("scp")
1534        .args([
1535            "-P", &ssh_port,
1536            "rustbasic.tar", &env_file,
1537            &format!("{}@{}:{}", ssh_user, ssh_ip, dest_dir)
1538        ])
1539        .status();
1540
1541    // Hapus file tar lokal
1542    let _ = std::fs::remove_file("rustbasic.tar");
1543
1544    if let Ok(status) = scp_status {
1545        if !status.success() {
1546            println!("{}", "āŒ Gagal mengirimkan berkas via SCP.".red().bold());
1547            return;
1548        }
1549    } else {
1550        println!("{}", "āŒ Gagal menjalankan SCP.".red().bold());
1551        return;
1552    }
1553
1554    println!("{}", "āœ… Pengiriman berkas berhasil!".green());
1555
1556    // 4. Eksekusi SSH otomatis di server jika disetujui
1557    let auto_run = prompt_string("\nšŸ‘‰ Apakah Anda ingin langsung menjalankan container di server secara otomatis? (y/n) [default: y]: ", "y");
1558    if auto_run.to_lowercase() == "y" || auto_run.is_empty() {
1559        println!("\nšŸš€ Memuat image di server (docker load)...");
1560        let load_status = std::process::Command::new("ssh")
1561            .args([
1562                "-p", &ssh_port,
1563                &format!("{}@{}", ssh_user, ssh_ip),
1564                &format!("docker load -i {}/rustbasic.tar", dest_dir)
1565            ])
1566            .status();
1567
1568        match load_status {
1569            Ok(status) if status.success() => {
1570                println!("{}", "āœ… Image berhasil dimuat di server.".green());
1571            }
1572            _ => {
1573                println!("{}", "āŒ Gagal memuat image di server.".red().bold());
1574                return;
1575            }
1576        }
1577
1578        println!("šŸš€ Menghentikan & menghapus container lama 'rustbasic-app' jika ada...");
1579        let stop_status = std::process::Command::new("ssh")
1580            .args([
1581                "-p", &ssh_port,
1582                &format!("{}@{}", ssh_user, ssh_ip),
1583                "docker stop rustbasic-app || true && docker rm rustbasic-app || true"
1584            ])
1585            .status();
1586
1587        if let Err(e) = stop_status {
1588            println!("āš ļø Peringatan saat membersihkan container lama: {}", e);
1589        }
1590
1591        println!("šŸš€ Menjalankan container baru 'rustbasic-app'...");
1592        let run_cmd = format!(
1593            "docker run -d --name rustbasic-app -p {} --restart unless-stopped --env-file {}/.env {}",
1594            server_port, dest_dir, image_name
1595        );
1596        let run_status = std::process::Command::new("ssh")
1597            .args([
1598                "-p", &ssh_port,
1599                &format!("{}@{}", ssh_user, ssh_ip),
1600                &run_cmd
1601            ])
1602            .status();
1603
1604        match run_status {
1605            Ok(status) if status.success() => {
1606                println!("{}", "šŸŽ‰ Container 'rustbasic-app' berhasil dijalankan di server!".green().bold());
1607            }
1608            _ => {
1609                println!("{}", "āŒ Gagal menjalankan container di server.".red().bold());
1610                return;
1611            }
1612        }
1613
1614        println!("šŸš€ Membersihkan file tar di server...");
1615        let rm_status = std::process::Command::new("ssh")
1616            .args([
1617                "-p", &ssh_port,
1618                &format!("{}@{}", ssh_user, ssh_ip),
1619                &format!("rm {}/rustbasic.tar", dest_dir)
1620            ])
1621            .status();
1622
1623        if let Err(e) = rm_status {
1624            println!("āš ļø Peringatan saat membersihkan file tar di server: {}", e);
1625        }
1626
1627        println!("\n{}", "šŸŽ‰ Deployment selesai!".green().bold());
1628        println!("{}", "--------------------------------------------------------".green());
1629        println!("Untuk melihat log aplikasi di server, jalankan:");
1630        println!("ssh -p {} {}@{} \"docker logs -f rustbasic-app\"", ssh_port, ssh_user, ssh_ip);
1631        println!("{}", "--------------------------------------------------------".green());
1632    } else {
1633        println!("\n{}", "šŸ–„ļø  Langkah Selanjutnya di Server Anda:".cyan().bold());
1634        println!("{}", "--------------------------------------------------------".green());
1635        println!("1. Hubungkan ke server via SSH:");
1636        println!("   ssh -p {} {}@{}", ssh_port, ssh_user, ssh_ip);
1637        println!("");
1638        println!("2. Masuk ke folder tujuan:");
1639        println!("   cd {}", dest_dir);
1640        println!("");
1641        println!("3. Muat (load) image Docker dari berkas tar:");
1642        println!("   docker load -i rustbasic.tar");
1643        println!("");
1644        println!("4. Jalankan container dengan fitur auto-restart:");
1645        println!("   docker run -d --name rustbasic-app -p {} --restart unless-stopped --env-file .env {}", server_port, image_name);
1646        println!("");
1647        println!("5. Hapus file tar di server untuk menghemat disk:");
1648        println!("   rm rustbasic.tar");
1649        println!("{}", "--------------------------------------------------------".green());
1650    }
1651}
1652
1653// ============================================================
1654// PUBLISH
1655// ============================================================
1656fn handle_publish(target: &str) {
1657    let mut selected_target = target.to_string();
1658    if selected_target.is_empty() {
1659        println!("šŸ› ļø  RustBasic Configuration Publisher");
1660        println!("Pilih konfigurasi yang ingin dipublikasikan ke proyek Anda:");
1661        println!("  [1] CORS (Cross-Origin Resource Sharing)");
1662        println!("  [2] CSRF (Cross-Site Request Forgery)");
1663        println!("  [3] APP (Application settings & Storage path overrides)");
1664        let choice = prompt_choice("šŸ‘‰ Pilih nomor konfigurasi (1-3): ", 1, 3);
1665        match choice {
1666            1 => selected_target = "cors".to_string(),
1667            2 => selected_target = "csrf".to_string(),
1668            3 => selected_target = "app".to_string(),
1669            _ => return,
1670        }
1671    }
1672
1673    match selected_target.as_str() {
1674        "cors" => {
1675            let path = std::path::Path::new("src/config/cors.rs");
1676            if path.exists() {
1677                println!("ā„¹ļø  File cors.rs sudah ada di src/config/cors.rs");
1678                return;
1679            }
1680            if let Some(parent) = path.parent() {
1681                let _ = std::fs::create_dir_all(parent);
1682            }
1683            let content = r#"/* ---------------------------------------------------------
1684 * šŸ“‘ LABEL: CORS CONFIGURATION (src/config/cors.rs)
1685 * Berkas konfigurasi tambahan untuk kustomisasi CORS.
1686 * --------------------------------------------------------- */
1687
1688pub struct CorsConfig {
1689    pub allowed_origins: Vec<&'static str>,
1690    pub allowed_methods: Vec<&'static str>,
1691    pub allowed_headers: Vec<&'static str>,
1692}
1693
1694impl Default for CorsConfig {
1695    fn default() -> Self {
1696        Self {
1697            allowed_origins: vec!["*"],
1698            allowed_methods: vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"],
1699            allowed_headers: vec!["*"],
1700        }
1701    }
1702}
1703"#;
1704            if std::fs::write(path, content).is_ok() {
1705                println!("{} {}", "āœ… Berhasil mempublikasikan konfigurasi CORS ke:".green().bold(), path.display().to_string().cyan());
1706                println!("šŸ’” File ini sekarang dapat diimpor untuk menyesuaikan aturan CORS lokal.");
1707            } else {
1708                println!("āŒ Gagal menulis file CORS config.");
1709            }
1710        }
1711        "csrf" => {
1712            let path = std::path::Path::new("src/config/csrf.rs");
1713            if path.exists() {
1714                println!("ā„¹ļø  File csrf.rs sudah ada di src/config/csrf.rs");
1715                return;
1716            }
1717            if let Some(parent) = path.parent() {
1718                let _ = std::fs::create_dir_all(parent);
1719            }
1720            let content = r#"/* ---------------------------------------------------------
1721 * šŸ“‘ LABEL: CSRF CONFIGURATION (src/config/csrf.rs)
1722 * Berkas konfigurasi tambahan untuk perlindungan CSRF.
1723 * --------------------------------------------------------- */
1724
1725pub struct CsrfConfig {
1726    pub except_paths: Vec<&'static str>,
1727}
1728
1729impl Default for CsrfConfig {
1730    fn default() -> Self {
1731        Self {
1732            except_paths: vec![], // Masukkan rute yang dikecualikan dari CSRF di sini
1733        }
1734    }
1735}
1736"#;
1737            if std::fs::write(path, content).is_ok() {
1738                println!("{} {}", "āœ… Berhasil mempublikasikan konfigurasi CSRF ke:".green().bold(), path.display().to_string().cyan());
1739                println!("šŸ’” File ini sekarang dapat diimpor untuk mengecualikan rute tertentu dari CSRF.");
1740            } else {
1741                println!("āŒ Gagal menulis file CSRF config.");
1742            }
1743        }
1744        "app" => {
1745            let path = std::path::Path::new("src/config/app.rs");
1746            if path.exists() {
1747                println!("ā„¹ļø  File app.rs sudah ada di src/config/app.rs");
1748                return;
1749            }
1750            if let Some(parent) = path.parent() {
1751                let _ = std::fs::create_dir_all(parent);
1752            }
1753            let content = r#"/* ---------------------------------------------------------
1754 * šŸ“‘ LABEL: APP CONFIGURATION (src/config/app.rs)
1755 * Berkas konfigurasi tambahan untuk kustomisasi lokasi storage lokal.
1756 * --------------------------------------------------------- */
1757
1758pub const STORAGE_TARGET: &str = "public/storage";
1759pub const STORAGE_SOURCE: &str = "storage/app/public";
1760"#;
1761            if std::fs::write(path, content).is_ok() {
1762                println!("{} {}", "āœ… Berhasil mempublikasikan konfigurasi APP ke:".green().bold(), path.display().to_string().cyan());
1763            } else {
1764                println!("āŒ Gagal menulis file APP config.");
1765            }
1766        }
1767        _ => {
1768            println!("āŒ Target '{}' tidak dikenal untuk di-publish.", selected_target);
1769            println!("šŸ’” Target yang didukung: cors, csrf, app");
1770        }
1771    }
1772}
1773
1774fn get_cargo_package_name() -> String {
1775    if let Ok(content) = std::fs::read_to_string("Cargo.toml") {
1776        for line in content.lines() {
1777            let trimmed = line.trim();
1778            if trimmed.starts_with("name =") || trimmed.starts_with("name=") {
1779                let parts: Vec<&str> = trimmed.split('=').collect();
1780                if parts.len() > 1 {
1781                    let name = parts[1].trim().trim_matches('"').trim_matches('\'');
1782                    return name.to_string();
1783                }
1784            }
1785        }
1786    }
1787    "rustbasic".to_string()
1788}
1789